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"});
|
var app = Elm.App.init({flags: "https://pbx-provisioning.sailmaker.fenix.lgbt/dashboard-kitchen.json"});
|
||||||
|
|
||||||
const simpleUserDelegate = {
|
const simpleUserDelegate = {
|
||||||
|
onServerConnect: () => {
|
||||||
|
app.ports.newConnectionState.send("connected");
|
||||||
|
},
|
||||||
|
onServerDisconnect: (error) => {
|
||||||
|
app.ports.newConnectionState.send(error ? "failed" : "disconnected");
|
||||||
|
},
|
||||||
onCallCreated: () => {
|
onCallCreated: () => {
|
||||||
console.log('Call created');
|
app.ports.newCallState.send("ringing");
|
||||||
|
},
|
||||||
|
onCallReceived: () => {
|
||||||
|
app.ports.newCallState.send("ringing");
|
||||||
},
|
},
|
||||||
onCallAnswered: () => {
|
onCallAnswered: () => {
|
||||||
console.log('Call answered');
|
app.ports.newCallState.send("on call");
|
||||||
},
|
|
||||||
onCallHangup: () => {
|
|
||||||
console.log('Call hung up');
|
|
||||||
},
|
},
|
||||||
onCallHold: (held) => {
|
onCallHold: (held) => {
|
||||||
console.log('Call hold: ${held}')
|
app.ports.newCallState.send("on hold");
|
||||||
|
},
|
||||||
|
onCallHangup: () => {
|
||||||
|
app.ports.newCallState.send(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var simpleUser;
|
var simpleUser;
|
||||||
app.ports.connectPhone.subscribe(function(config) {
|
app.ports.connect.subscribe(function(config) {
|
||||||
console.log('Connect', config);
|
console.log('Connect', config);
|
||||||
const simpleUserOptions = {
|
const simpleUserOptions = {
|
||||||
delegate: simpleUserDelegate,
|
delegate: simpleUserDelegate,
|
||||||
|
@ -63,6 +72,9 @@
|
||||||
);
|
);
|
||||||
simpleUser.connect();
|
simpleUser.connect();
|
||||||
});
|
});
|
||||||
|
app.ports.dial.subscribe(function(address) {
|
||||||
|
simpleUser.call(address);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 Browser
|
||||||
import Element as E
|
import Element as E
|
||||||
|
import Element.Background as Background
|
||||||
import Html
|
import Html
|
||||||
import Html.Attributes
|
import Html.Attributes
|
||||||
import Http
|
import Http
|
||||||
import Json.Decode
|
import Json.Decode
|
||||||
import Json.Decode.Pipeline as JDP
|
import Phone
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
type Extension
|
type Extension
|
||||||
= Unconfigured
|
= Unconfigured
|
||||||
| ConfigurationError
|
| ConfigurationError
|
||||||
| Configured Configuration ExtensionState
|
| Configured Phone.Model
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
|
@ -69,7 +22,9 @@ type alias Model =
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= GotConfiguration (Result Http.Error Configuration)
|
= GotConfiguration (Result Http.Error Phone.Configuration)
|
||||||
|
| GotConnectionState Json.Decode.Value
|
||||||
|
| PhoneMsg Phone.Msg
|
||||||
|
|
||||||
|
|
||||||
main =
|
main =
|
||||||
|
@ -86,7 +41,7 @@ init provisioningUrl =
|
||||||
( { extension = Unconfigured }
|
( { extension = Unconfigured }
|
||||||
, Http.get
|
, Http.get
|
||||||
{ url = provisioningUrl
|
{ url = provisioningUrl
|
||||||
, expect = Http.expectJson GotConfiguration decodeConfiguration
|
, expect = Http.expectJson GotConfiguration Phone.decodeConfiguration
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,8 +50,8 @@ view : Model -> Browser.Document Msg
|
||||||
view model =
|
view model =
|
||||||
{ title = "Dashboard"
|
{ title = "Dashboard"
|
||||||
, body =
|
, body =
|
||||||
[ E.layout [] <|
|
[ E.layout [ E.height E.fill ] <|
|
||||||
E.column []
|
E.column [ E.height E.fill, E.width E.fill ]
|
||||||
[ E.html (Html.audio [ Html.Attributes.id "remoteAudio" ] [])
|
[ E.html (Html.audio [ Html.Attributes.id "remoteAudio" ] [])
|
||||||
, case model.extension of
|
, case model.extension of
|
||||||
Unconfigured ->
|
Unconfigured ->
|
||||||
|
@ -105,8 +60,8 @@ view model =
|
||||||
ConfigurationError ->
|
ConfigurationError ->
|
||||||
E.text "Configuration failed."
|
E.text "Configuration failed."
|
||||||
|
|
||||||
Configured config state ->
|
Configured phone ->
|
||||||
E.text "Ready!"
|
E.map PhoneMsg <| Phone.view phone
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -116,15 +71,32 @@ update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
update msg model =
|
update msg model =
|
||||||
case msg of
|
case msg of
|
||||||
GotConfiguration (Ok config) ->
|
GotConfiguration (Ok config) ->
|
||||||
( { model | extension = Configured config Disconnected }, connectPhone config )
|
( { model | extension = Configured (Phone.init config) }, Phone.connect config )
|
||||||
|
|
||||||
GotConfiguration (Err _) ->
|
GotConfiguration (Err _) ->
|
||||||
( { model | extension = ConfigurationError }, Cmd.none )
|
( { 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 Msg
|
||||||
subscriptions model =
|
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