Add a simple contact calling interface

This commit is contained in:
Correl Roush 2024-12-22 01:55:04 -05:00
parent 9e87691a42
commit 8326c60e53
3 changed files with 296 additions and 68 deletions

View file

@ -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>

View file

@ -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
View 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