Add a simple contact calling interface
This commit is contained in:
parent
9e87691a42
commit
8326c60e53
3 changed files with 296 additions and 68 deletions
|
@ -26,21 +26,30 @@
|
|||
var app = Elm.App.init({flags: "https://pbx-provisioning.sailmaker.fenix.lgbt/dashboard-kitchen.json"});
|
||||
|
||||
const simpleUserDelegate = {
|
||||
onServerConnect: () => {
|
||||
app.ports.newConnectionState.send("connected");
|
||||
},
|
||||
onServerDisconnect: (error) => {
|
||||
app.ports.newConnectionState.send(error ? "failed" : "disconnected");
|
||||
},
|
||||
onCallCreated: () => {
|
||||
console.log('Call created');
|
||||
app.ports.newCallState.send("ringing");
|
||||
},
|
||||
onCallReceived: () => {
|
||||
app.ports.newCallState.send("ringing");
|
||||
},
|
||||
onCallAnswered: () => {
|
||||
console.log('Call answered');
|
||||
},
|
||||
onCallHangup: () => {
|
||||
console.log('Call hung up');
|
||||
app.ports.newCallState.send("on call");
|
||||
},
|
||||
onCallHold: (held) => {
|
||||
console.log('Call hold: ${held}')
|
||||
app.ports.newCallState.send("on hold");
|
||||
},
|
||||
onCallHangup: () => {
|
||||
app.ports.newCallState.send(null);
|
||||
}
|
||||
}
|
||||
var simpleUser;
|
||||
app.ports.connectPhone.subscribe(function(config) {
|
||||
app.ports.connect.subscribe(function(config) {
|
||||
console.log('Connect', config);
|
||||
const simpleUserOptions = {
|
||||
delegate: simpleUserDelegate,
|
||||
|
@ -63,6 +72,9 @@
|
|||
);
|
||||
simpleUser.connect();
|
||||
});
|
||||
app.ports.dial.subscribe(function(address) {
|
||||
simpleUser.call(address);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
94
src/App.elm
94
src/App.elm
|
@ -1,66 +1,19 @@
|
|||
port module App exposing (main)
|
||||
module App exposing (main)
|
||||
|
||||
import Browser
|
||||
import Element as E
|
||||
import Element.Background as Background
|
||||
import Html
|
||||
import Html.Attributes
|
||||
import Http
|
||||
import Json.Decode
|
||||
import Json.Decode.Pipeline as JDP
|
||||
|
||||
|
||||
type alias Contact =
|
||||
{ name : String
|
||||
, number : String
|
||||
}
|
||||
|
||||
|
||||
decodeContact : Json.Decode.Decoder Contact
|
||||
decodeContact =
|
||||
Json.Decode.succeed Contact
|
||||
|> JDP.required "name" Json.Decode.string
|
||||
|> JDP.required "number" Json.Decode.string
|
||||
|
||||
|
||||
type alias Configuration =
|
||||
{ name : String
|
||||
, websocket : String
|
||||
, domain : String
|
||||
, username : String
|
||||
, password : String
|
||||
, voicemail : Maybe String
|
||||
, contacts : List Contact
|
||||
}
|
||||
|
||||
|
||||
decodeConfiguration : Json.Decode.Decoder Configuration
|
||||
decodeConfiguration =
|
||||
Json.Decode.succeed Configuration
|
||||
|> JDP.required "name" Json.Decode.string
|
||||
|> JDP.required "websocket" Json.Decode.string
|
||||
|> JDP.required "domain" Json.Decode.string
|
||||
|> JDP.required "username" Json.Decode.string
|
||||
|> JDP.required "password" Json.Decode.string
|
||||
|> JDP.required "voicemail" (Json.Decode.nullable Json.Decode.string)
|
||||
|> JDP.required "contacts" (Json.Decode.list decodeContact)
|
||||
|
||||
|
||||
type alias CallerId =
|
||||
{ name : Maybe String
|
||||
, number : String
|
||||
}
|
||||
|
||||
|
||||
type ExtensionState
|
||||
= Disconnected
|
||||
| Ringing CallerId
|
||||
| Connected CallerId
|
||||
import Phone
|
||||
|
||||
|
||||
type Extension
|
||||
= Unconfigured
|
||||
| ConfigurationError
|
||||
| Configured Configuration ExtensionState
|
||||
| Configured Phone.Model
|
||||
|
||||
|
||||
type alias Model =
|
||||
|
@ -69,7 +22,9 @@ type alias Model =
|
|||
|
||||
|
||||
type Msg
|
||||
= GotConfiguration (Result Http.Error Configuration)
|
||||
= GotConfiguration (Result Http.Error Phone.Configuration)
|
||||
| GotConnectionState Json.Decode.Value
|
||||
| PhoneMsg Phone.Msg
|
||||
|
||||
|
||||
main =
|
||||
|
@ -86,7 +41,7 @@ init provisioningUrl =
|
|||
( { extension = Unconfigured }
|
||||
, Http.get
|
||||
{ url = provisioningUrl
|
||||
, expect = Http.expectJson GotConfiguration decodeConfiguration
|
||||
, expect = Http.expectJson GotConfiguration Phone.decodeConfiguration
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -95,8 +50,8 @@ view : Model -> Browser.Document Msg
|
|||
view model =
|
||||
{ title = "Dashboard"
|
||||
, body =
|
||||
[ E.layout [] <|
|
||||
E.column []
|
||||
[ E.layout [ E.height E.fill ] <|
|
||||
E.column [ E.height E.fill, E.width E.fill ]
|
||||
[ E.html (Html.audio [ Html.Attributes.id "remoteAudio" ] [])
|
||||
, case model.extension of
|
||||
Unconfigured ->
|
||||
|
@ -105,8 +60,8 @@ view model =
|
|||
ConfigurationError ->
|
||||
E.text "Configuration failed."
|
||||
|
||||
Configured config state ->
|
||||
E.text "Ready!"
|
||||
Configured phone ->
|
||||
E.map PhoneMsg <| Phone.view phone
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -116,15 +71,32 @@ update : Msg -> Model -> ( Model, Cmd Msg )
|
|||
update msg model =
|
||||
case msg of
|
||||
GotConfiguration (Ok config) ->
|
||||
( { model | extension = Configured config Disconnected }, connectPhone config )
|
||||
( { model | extension = Configured (Phone.init config) }, Phone.connect config )
|
||||
|
||||
GotConfiguration (Err _) ->
|
||||
( { model | extension = ConfigurationError }, Cmd.none )
|
||||
|
||||
GotConnectionState value ->
|
||||
( model, Cmd.none )
|
||||
|
||||
PhoneMsg phoneMsg ->
|
||||
case model.extension of
|
||||
Configured phone ->
|
||||
let
|
||||
( newPhone, phoneCmd ) =
|
||||
Phone.update phoneMsg phone
|
||||
in
|
||||
( { model | extension = Configured newPhone }, Cmd.map PhoneMsg phoneCmd )
|
||||
|
||||
_ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.none
|
||||
case model.extension of
|
||||
Configured phone ->
|
||||
Sub.map PhoneMsg <| Phone.subscriptions phone
|
||||
|
||||
|
||||
port connectPhone : Configuration -> Cmd msg
|
||||
_ ->
|
||||
Sub.none
|
||||
|
|
244
src/Phone.elm
Normal file
244
src/Phone.elm
Normal file
|
@ -0,0 +1,244 @@
|
|||
port module Phone exposing
|
||||
( Configuration
|
||||
, ConnectionState
|
||||
, Contact
|
||||
, Model
|
||||
, Msg
|
||||
, connect
|
||||
, decodeConfiguration
|
||||
, decodeConnectionState
|
||||
, init
|
||||
, newConnectionState
|
||||
, subscriptions
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Element as E
|
||||
import Element.Background as Background
|
||||
import Element.Border as Border
|
||||
import Element.Events as Events
|
||||
import Element.Font as Font
|
||||
import Json.Decode
|
||||
import Json.Decode.Pipeline as JDP
|
||||
|
||||
|
||||
type alias Contact =
|
||||
{ name : String
|
||||
, number : String
|
||||
}
|
||||
|
||||
|
||||
type alias Configuration =
|
||||
{ name : String
|
||||
, websocket : String
|
||||
, domain : String
|
||||
, username : String
|
||||
, password : String
|
||||
, voicemail : Maybe String
|
||||
, contacts : List Contact
|
||||
}
|
||||
|
||||
|
||||
type ConnectionState
|
||||
= Disconnected
|
||||
| ConnectionFailed
|
||||
| Connected
|
||||
|
||||
|
||||
type CallDirection
|
||||
= Incoming
|
||||
| Outgoing
|
||||
|
||||
|
||||
type CallState
|
||||
= Ringing
|
||||
| OnCall
|
||||
| OnHold
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ configuration : Configuration
|
||||
, connection : ConnectionState
|
||||
, call : Maybe CallState
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= NewConnectionState Json.Decode.Value
|
||||
| NewCallState Json.Decode.Value
|
||||
| ContactSelected Contact
|
||||
|
||||
|
||||
decodeContact : Json.Decode.Decoder Contact
|
||||
decodeContact =
|
||||
Json.Decode.succeed Contact
|
||||
|> JDP.required "name" Json.Decode.string
|
||||
|> JDP.required "number" Json.Decode.string
|
||||
|
||||
|
||||
decodeConfiguration : Json.Decode.Decoder Configuration
|
||||
decodeConfiguration =
|
||||
Json.Decode.succeed Configuration
|
||||
|> JDP.required "name" Json.Decode.string
|
||||
|> JDP.required "websocket" Json.Decode.string
|
||||
|> JDP.required "domain" Json.Decode.string
|
||||
|> JDP.required "username" Json.Decode.string
|
||||
|> JDP.required "password" Json.Decode.string
|
||||
|> JDP.required "voicemail" (Json.Decode.nullable Json.Decode.string)
|
||||
|> JDP.required "contacts" (Json.Decode.list decodeContact)
|
||||
|
||||
|
||||
decodeConnectionState : Json.Decode.Decoder ConnectionState
|
||||
decodeConnectionState =
|
||||
let
|
||||
fromString : String -> Json.Decode.Decoder ConnectionState
|
||||
fromString state =
|
||||
case state of
|
||||
"connected" ->
|
||||
Json.Decode.succeed Connected
|
||||
|
||||
"disconnected" ->
|
||||
Json.Decode.succeed Disconnected
|
||||
|
||||
"failed" ->
|
||||
Json.Decode.succeed ConnectionFailed
|
||||
|
||||
_ ->
|
||||
Json.Decode.fail <| "Unexpected connection state " ++ state
|
||||
in
|
||||
Json.Decode.string
|
||||
|> Json.Decode.andThen fromString
|
||||
|
||||
|
||||
decodeCallState : Json.Decode.Decoder CallState
|
||||
decodeCallState =
|
||||
let
|
||||
fromString : String -> Json.Decode.Decoder CallState
|
||||
fromString state =
|
||||
case state of
|
||||
"ringing" ->
|
||||
Json.Decode.succeed Ringing
|
||||
|
||||
"on call" ->
|
||||
Json.Decode.succeed OnCall
|
||||
|
||||
"on hold" ->
|
||||
Json.Decode.succeed OnHold
|
||||
|
||||
_ ->
|
||||
Json.Decode.fail <| "Unexpected call state " ++ state
|
||||
in
|
||||
Json.Decode.string
|
||||
|> Json.Decode.andThen fromString
|
||||
|
||||
|
||||
init : Configuration -> Model
|
||||
init config =
|
||||
{ configuration = config
|
||||
, connection = Disconnected
|
||||
, call = Nothing
|
||||
}
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
NewConnectionState value ->
|
||||
case Json.Decode.decodeValue decodeConnectionState value of
|
||||
Ok connection ->
|
||||
( { model | connection = connection }, Cmd.none )
|
||||
|
||||
Err _ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
NewCallState value ->
|
||||
case Json.Decode.decodeValue (Json.Decode.nullable decodeCallState) value of
|
||||
Ok call ->
|
||||
( { model | call = call }, Cmd.none )
|
||||
|
||||
Err _ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
ContactSelected contact ->
|
||||
case model.connection of
|
||||
Connected ->
|
||||
( model, dial ("sip:" ++ contact.number ++ "@" ++ model.configuration.domain) )
|
||||
|
||||
_ ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
view : Model -> E.Element Msg
|
||||
view model =
|
||||
let
|
||||
viewContact : Contact -> E.Element Msg
|
||||
viewContact contact =
|
||||
E.row
|
||||
[ E.width E.fill
|
||||
, E.padding 30
|
||||
, E.spacing 30
|
||||
, Border.widthEach { bottom = 1, top = 0, left = 0, right = 0 }
|
||||
, E.pointer
|
||||
, E.mouseOver [ Background.color <| E.rgb255 200 200 200 ]
|
||||
, Events.onClick <| ContactSelected contact
|
||||
]
|
||||
[ E.el [] <| E.text contact.number
|
||||
, E.el [ E.width E.fill ] <| E.text contact.name
|
||||
]
|
||||
in
|
||||
E.column [ E.width E.fill, E.height E.fill ]
|
||||
[ E.row [ E.width E.fill, E.padding 10, Border.solid, Border.widthEach { bottom = 3, top = 0, left = 0, right = 0 } ]
|
||||
[ E.el [ E.width <| E.px 50 ] <|
|
||||
E.text <|
|
||||
case model.connection of
|
||||
Disconnected ->
|
||||
"◯"
|
||||
|
||||
_ ->
|
||||
"🟢"
|
||||
, E.el [ E.centerX ] <| E.text model.configuration.name
|
||||
]
|
||||
, case model.call of
|
||||
Just call ->
|
||||
E.el
|
||||
[ E.centerX
|
||||
, E.centerY
|
||||
, Font.size 120
|
||||
, Font.color <|
|
||||
case call of
|
||||
Ringing ->
|
||||
E.rgb255 255 255 0
|
||||
|
||||
OnCall ->
|
||||
E.rgb255 0 255 0
|
||||
|
||||
OnHold ->
|
||||
E.rgb255 0 0 255
|
||||
]
|
||||
<|
|
||||
E.text "☎"
|
||||
|
||||
Nothing ->
|
||||
E.column [ E.width E.fill ] <| List.map viewContact model.configuration.contacts
|
||||
]
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.batch
|
||||
[ newConnectionState NewConnectionState
|
||||
, newCallState NewCallState
|
||||
]
|
||||
|
||||
|
||||
port connect : Configuration -> Cmd msg
|
||||
|
||||
|
||||
port dial : String -> Cmd msg
|
||||
|
||||
|
||||
port newConnectionState : (Json.Decode.Value -> msg) -> Sub msg
|
||||
|
||||
|
||||
port newCallState : (Json.Decode.Value -> msg) -> Sub msg
|
Loading…
Reference in a new issue