diff --git a/docs/rfcs/webrtc.mmd b/docs/rfcs/webrtc.mmd
new file mode 100644
index 0000000000..4da2841e33
--- /dev/null
+++ b/docs/rfcs/webrtc.mmd
@@ -0,0 +1,67 @@
+sequenceDiagram
+ participant AW as Alice's
web view
+ participant AN as Alice's
app native
+ participant AC as Alice's
chat core
+ participant BC as Bob's
chat core
+ participant BN as Bob's
app native
+ participant BW as Bob's
web view
+
+ note over AW, AC: Alice's app
+ note over BC, BW: Bob's app
+
+ note over AW, BW: 1. Establishing call
+
+ note over AN: user: start call
+ AN ->> AW: WCCapabilities
+ AW ->> AN: WRCapabilities (e2e?)
+
+ AN ->> AC: APISendCallInvitation
+ AC -->> BC: XCallInv
+ AC ->> AN: CRCmdOk
+
+ BC ->> BN: CRCallInvitation
+ note over BN: show: accept call?
+
+ alt user accepted?
+ BN ->> BC: no: APIRejectCall
(sender not notified)
+ BC ->> BN: CRCmdOk
+ else
+ BN ->> BW: yes: WCStartCall
+ BW ->> BN: WCallOffer
+ end
+
+ BN ->> BC: APISendCallOffer
+ BC -->> AC: XCallOffer
+ BC ->> BN: CRCmdOk
+ AC ->> AN: CRCallOffer
+ note over AN: show if no e2e: continue call?
+
+ AN ->> AW: WCallOffer
+ AW ->> AN: WCallAnswer
+ AN ->> AC: APISendCallAnswer
+ AC -->> BC: XCallAnswer
+ AC ->> AN: CRCmdOk
+ BC ->> BN: CRCallAnswer
+ BN ->> BW: WCallAnswer
+
+ note over AW, BW: call can be established at this point
+
+ note over AW, BW: 2. Sending additional ice candidates
(optional, same for another side):
+
+ BW ->> BN: WCallICE
+ BN ->> BC: APISendCallExtraInfo
+ BC -->> AC: XCallExtra
+ BC ->> BN: CRCmdOk
+ AC ->> AN: CRCallExtraInfo
+ AN ->> AW: WCallICE
+
+ note over AW, BW: 3. Call termination (same for another party):
+
+ note over AN: user: end call
+ AN ->> AW: WEndCall
+ AN ->> AC: APIEndCall
+ AC -->> BC: XCallEnd
+ AC ->> AN: CRCmdOk
+ BC ->> BN: CRCallEnded
+ note over BN: show: call ended
+ BN ->> BW: WEndCall
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index 92baa1eb24..87d37efeff 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -21,6 +21,7 @@ library
exposed-modules:
Simplex.Chat
Simplex.Chat.Bot
+ Simplex.Chat.Call
Simplex.Chat.Controller
Simplex.Chat.Core
Simplex.Chat.Help
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 49963a22de..721c0f0add 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -123,8 +123,9 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize} Ch
chatLock <- newTMVarIO ()
sndFiles <- newTVarIO M.empty
rcvFiles <- newTVarIO M.empty
+ currentCall <- newTVarIO Nothing
filesFolder <- newTVarIO Nothing
- pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, config, sendNotification, filesFolder}
+ pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCall, config, sendNotification, filesFolder}
where
resolveServers :: IO (NonEmpty SMPServer)
resolveServers = case user of
@@ -388,6 +389,12 @@ processChatCommand = \case
`E.finally` deleteContactRequest st userId connReqId
withAgent $ \a -> rejectContact a connId invId
pure $ CRContactRequestRejected cReq
+ APISendCallInvitation _contactId _callType -> pure $ chatCmdError "not implemented"
+ APIRejectCall _contactId -> pure $ chatCmdError "not implemented"
+ APISendCallOffer _contactId _wCallOffer -> pure $ chatCmdError "not implemented"
+ APISendCallAnswer _contactId _rtcSession -> pure $ chatCmdError "not implemented"
+ APISendCallExtraInfo _contactId _rtcExtraInfo -> pure $ chatCmdError "not implemented"
+ APIEndCall _contactId -> pure $ chatCmdError "not implemented"
APIUpdateProfile profile -> withUser (`updateProfile` profile)
APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text
APIRegisterToken token -> CRNtfTokenStatus <$> withUser (\_ -> withAgent (`registerNtfToken` token))
@@ -1881,6 +1888,12 @@ chatCommandP =
<|> "/_delete " *> (APIDeleteChat <$> chatRefP)
<|> "/_accept " *> (APIAcceptContact <$> A.decimal)
<|> "/_reject " *> (APIRejectContact <$> A.decimal)
+ <|> "/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP)
+ <|> "/_call reject @" *> (APIRejectCall <$> A.decimal)
+ <|> "/_call offer @" *> (APISendCallOffer <$> A.decimal <* A.space <*> jsonP)
+ <|> "/_call answer @" *> (APISendCallAnswer <$> A.decimal <* A.space <*> jsonP)
+ <|> "/_call extra @" *> (APISendCallExtraInfo <$> A.decimal <* A.space <*> jsonP)
+ <|> "/_call end @" *> (APIEndCall <$> A.decimal)
<|> "/_profile " *> (APIUpdateProfile <$> jsonP)
<|> "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString)
<|> "/_ntf register " *> (APIRegisterToken <$> tokenP)
diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs
new file mode 100644
index 0000000000..9461ff505f
--- /dev/null
+++ b/src/Simplex/Chat/Call.hs
@@ -0,0 +1,170 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Simplex.Chat.Call where
+
+import Data.Aeson (FromJSON, ToJSON)
+import qualified Data.Aeson as J
+import Data.ByteString.Char8 (ByteString)
+import Data.Int (Int64)
+import GHC.Generics (Generic)
+import qualified Simplex.Messaging.Crypto as C
+import Simplex.Messaging.Encoding.String
+import Simplex.Messaging.Parsers (dropPrefix, enumJSON)
+
+data Call = Call
+ { contactId :: Int64,
+ callId :: CallId,
+ chatItemId :: Int64,
+ callState :: CallState
+ }
+
+data CallState
+ = CallInvitationSent
+ { localCallType :: CallType,
+ localDhPrivKey :: Maybe C.PrivateKeyX25519
+ }
+ | CallInvitationReceived
+ { peerCallType :: CallType,
+ localDhPubKey :: Maybe C.PublicKeyX25519,
+ sharedKey :: Maybe C.Key
+ }
+ | CallOfferSent
+ { localCallType :: CallType,
+ peerCallType :: CallType,
+ localCallSession :: WebRTCSession,
+ sharedKey :: Maybe C.Key
+ }
+ | CallOfferReceived
+ { localCallType :: CallType,
+ peerCallType :: CallType,
+ peerCallSession :: WebRTCSession,
+ sharedKey :: Maybe C.Key
+ }
+ | CallNegotiated
+ { localCallType :: CallType,
+ peerCallType :: CallType,
+ localCallSession :: WebRTCSession,
+ peerCallSession :: WebRTCSession,
+ sharedKey :: Maybe C.Key
+ }
+
+newtype CallId = CallId ByteString
+ deriving (Eq, Show)
+
+instance StrEncoding CallId where
+ strEncode (CallId m) = strEncode m
+ strDecode s = CallId <$> strDecode s
+ strP = CallId <$> strP
+
+instance FromJSON CallId where
+ parseJSON = strParseJSON "CallId"
+
+instance ToJSON CallId where
+ toJSON = strToJSON
+ toEncoding = strToJEncoding
+
+data CallType = CallType
+ { media :: CallMedia,
+ capabilities :: CallCapabilities
+ }
+ deriving (Eq, Show, Generic, FromJSON)
+
+instance ToJSON CallType where toEncoding = J.genericToEncoding J.defaultOptions
+
+-- | * Types for chat protocol
+data CallInvitation = CallInvitation
+ { callType :: CallType,
+ callDhPubKey :: Maybe C.PublicKeyX25519
+ }
+ deriving (Eq, Show, Generic)
+
+instance FromJSON CallInvitation where
+ parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
+
+instance ToJSON CallInvitation where
+ toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
+ toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
+
+data CallMedia = CMAudio | CMVideo
+ deriving (Eq, Show, Generic)
+
+instance FromJSON CallMedia where
+ parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CM"
+
+instance ToJSON CallMedia where
+ toJSON = J.genericToJSON . enumJSON $ dropPrefix "CM"
+ toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CM"
+
+data CallCapabilities = CallCapabilities
+ { encryption :: Bool
+ }
+ deriving (Eq, Show, Generic, FromJSON)
+
+instance ToJSON CallCapabilities where
+ toJSON = J.genericToJSON J.defaultOptions
+ toEncoding = J.genericToEncoding J.defaultOptions
+
+data CallOffer = CallOffer
+ { callType :: CallType,
+ rtcSession :: WebRTCSession,
+ callDhPubKey :: Maybe C.PublicKeyX25519
+ }
+ deriving (Eq, Show, Generic)
+
+instance FromJSON CallOffer where
+ parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
+
+instance ToJSON CallOffer where
+ toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
+ toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
+
+data WebRTCCallOffer = WebRTCCallOffer
+ { callType :: CallType,
+ rtcSession :: WebRTCSession
+ }
+ deriving (Eq, Show, Generic)
+
+instance FromJSON WebRTCCallOffer where
+ parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
+
+data CallAnswer = CallAnswer
+ { rtcSession :: WebRTCSession
+ }
+ deriving (Eq, Show, Generic, FromJSON)
+
+instance ToJSON CallAnswer where
+ toJSON = J.genericToJSON J.defaultOptions
+ toEncoding = J.genericToEncoding J.defaultOptions
+
+data CallExtraInfo = CallExtraInfo
+ { rtcExtraInfo :: WebRTCExtraInfo
+ }
+ deriving (Eq, Show, Generic, FromJSON)
+
+instance ToJSON CallExtraInfo where
+ toJSON = J.genericToJSON J.defaultOptions
+ toEncoding = J.genericToEncoding J.defaultOptions
+
+data WebRTCSession = WebRTCSession
+ { rtcSession :: J.Value,
+ rtcIceCandidates :: [J.Value]
+ }
+ deriving (Eq, Show, Generic, FromJSON)
+
+instance ToJSON WebRTCSession where
+ toJSON = J.genericToJSON J.defaultOptions
+ toEncoding = J.genericToEncoding J.defaultOptions
+
+data WebRTCExtraInfo = WebRTCExtraInfo
+ { rtcIceCandidates :: [J.Value]
+ }
+ deriving (Eq, Show, Generic, FromJSON)
+
+instance ToJSON WebRTCExtraInfo where
+ toJSON = J.genericToJSON J.defaultOptions
+ toEncoding = J.genericToEncoding J.defaultOptions
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 4c0c667c02..1339256d1d 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -26,6 +26,7 @@ import Data.Word (Word16)
import GHC.Generics (Generic)
import Numeric.Natural
import qualified Paths_simplex_chat as SC
+import Simplex.Chat.Call
import Simplex.Chat.Markdown (MarkdownList)
import Simplex.Chat.Messages
import Simplex.Chat.Protocol
@@ -80,6 +81,7 @@ data ChatController = ChatController
chatLock :: TMVar (),
sndFiles :: TVar (Map Int64 Handle),
rcvFiles :: TVar (Map Int64 Handle),
+ currentCall :: TVar (Maybe Call),
config :: ChatConfig,
filesFolder :: TVar (Maybe FilePath) -- path to files folder for mobile apps
}
@@ -108,6 +110,12 @@ data ChatCommand
| APIDeleteChat ChatRef
| APIAcceptContact Int64
| APIRejectContact Int64
+ | APISendCallInvitation Int64 CallType
+ | APIRejectCall Int64
+ | APISendCallOffer Int64 WebRTCCallOffer
+ | APISendCallAnswer Int64 WebRTCSession
+ | APISendCallExtraInfo Int64 WebRTCExtraInfo
+ | APIEndCall Int64
| APIUpdateProfile Profile
| APIParseMarkdown Text
| APIRegisterToken DeviceToken
@@ -240,6 +248,11 @@ data ChatResponse
| CRPendingSubSummary {pendingSubStatus :: [PendingSubStatus]}
| CRSndFileSubError {sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
| CRRcvFileSubError {rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
+ | CRCallInvitation {contact :: Contact, callType :: CallType, encryptionKey :: Maybe C.Key}
+ | CRCallOffer {contact :: Contact, callType :: CallType, offer :: WebRTCSession, encryptionKey :: Maybe C.Key, askConfirmation :: Bool}
+ | CRCallAnswer {contact :: Contact, answer :: WebRTCSession}
+ | CRCallExtraInfo {contact :: Contact, extraInfo :: WebRTCExtraInfo}
+ | CRCallEnded {contact :: Contact}
| CRUserContactLinkSubscribed
| CRUserContactLinkSubError {chatError :: ChatError}
| CRNtfTokenStatus {status :: NtfTknStatus}
diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs
index a54e779fb2..db8efceac3 100644
--- a/src/Simplex/Chat/Messages.hs
+++ b/src/Simplex/Chat/Messages.hs
@@ -20,6 +20,7 @@ import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Int (Int64)
import Data.Text (Text)
+import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay)
import Data.Time.LocalTime (TimeZone, ZonedTime, utcToZonedTime)
@@ -439,6 +440,8 @@ data CIContent (d :: MsgDirection) where
CIRcvMsgContent :: MsgContent -> CIContent 'MDRcv
CISndDeleted :: CIDeleteMode -> CIContent 'MDSnd
CIRcvDeleted :: CIDeleteMode -> CIContent 'MDRcv
+ CISndCall :: CICallStatus -> Int -> CIContent 'MDSnd
+ CIRcvCall :: CICallStatus -> Int -> CIContent 'MDRcv
deriving instance Show (CIContent d)
@@ -448,6 +451,8 @@ ciContentToText = \case
CIRcvMsgContent mc -> msgContentText mc
CISndDeleted cidm -> ciDeleteModeToText cidm
CIRcvDeleted cidm -> ciDeleteModeToText cidm
+ CISndCall status duration -> "outgoing call: " <> ciCallInfoText status duration
+ CIRcvCall status duration -> "incoming call: " <> ciCallInfoText status duration
msgDirToDeletedContent_ :: SMsgDirection d -> CIDeleteMode -> CIContent d
msgDirToDeletedContent_ msgDir mode = case msgDir of
@@ -480,6 +485,8 @@ data JSONCIContent
| JCIRcvMsgContent {msgContent :: MsgContent}
| JCISndDeleted {deleteMode :: CIDeleteMode}
| JCIRcvDeleted {deleteMode :: CIDeleteMode}
+ | JCISndCall {status :: CICallStatus, duration :: Int} -- duration in seconds
+ | JCIRcvCall {status :: CICallStatus, duration :: Int}
deriving (Generic)
instance FromJSON JSONCIContent where
@@ -495,6 +502,8 @@ jsonCIContent = \case
CIRcvMsgContent mc -> JCIRcvMsgContent mc
CISndDeleted cidm -> JCISndDeleted cidm
CIRcvDeleted cidm -> JCIRcvDeleted cidm
+ CISndCall status duration -> JCISndCall {status, duration}
+ CIRcvCall status duration -> JCIRcvCall {status, duration}
aciContentJSON :: JSONCIContent -> ACIContent
aciContentJSON = \case
@@ -502,6 +511,8 @@ aciContentJSON = \case
JCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc
JCISndDeleted cidm -> ACIContent SMDSnd $ CISndDeleted cidm
JCIRcvDeleted cidm -> ACIContent SMDRcv $ CIRcvDeleted cidm
+ JCISndCall {status, duration} -> ACIContent SMDSnd $ CISndCall status duration
+ JCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration
-- platform independent
data DBJSONCIContent
@@ -509,6 +520,8 @@ data DBJSONCIContent
| DBJCIRcvMsgContent {msgContent :: MsgContent}
| DBJCISndDeleted {deleteMode :: CIDeleteMode}
| DBJCIRcvDeleted {deleteMode :: CIDeleteMode}
+ | DBJCISndCall {status :: CICallStatus, duration :: Int}
+ | DBJCIRcvCall {status :: CICallStatus, duration :: Int}
deriving (Generic)
instance FromJSON DBJSONCIContent where
@@ -524,6 +537,8 @@ dbJsonCIContent = \case
CIRcvMsgContent mc -> DBJCIRcvMsgContent mc
CISndDeleted cidm -> DBJCISndDeleted cidm
CIRcvDeleted cidm -> DBJCIRcvDeleted cidm
+ CISndCall status duration -> DBJCISndCall {status, duration}
+ CIRcvCall status duration -> DBJCIRcvCall {status, duration}
aciContentDBJSON :: DBJSONCIContent -> ACIContent
aciContentDBJSON = \case
@@ -531,6 +546,35 @@ aciContentDBJSON = \case
DBJCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc
DBJCISndDeleted cidm -> ACIContent SMDSnd $ CISndDeleted cidm
DBJCIRcvDeleted cidm -> ACIContent SMDRcv $ CIRcvDeleted cidm
+ DBJCISndCall {status, duration} -> ACIContent SMDSnd $ CISndCall status duration
+ DBJCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration
+
+data CICallStatus
+ = CISCallPending
+ | CISCallMissed
+ | CISCallRejected -- only possible for received calls, not on type level
+ | CISCallProgress
+ | CISCallEnded
+ | CISCallError
+ deriving (Show, Generic)
+
+instance FromJSON CICallStatus where
+ parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CISCall"
+
+instance ToJSON CICallStatus where
+ toJSON = J.genericToJSON . enumJSON $ dropPrefix "CISCall"
+ toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CISCall"
+
+ciCallInfoText :: CICallStatus -> Int -> Text
+ciCallInfoText status duration = case status of
+ CISCallPending -> "calling..."
+ CISCallMissed -> "missed"
+ CISCallRejected -> "rejected"
+ CISCallProgress -> "in progress " <> d
+ CISCallEnded -> "ended " <> d
+ CISCallError -> "error"
+ where
+ d = let (mins, secs) = duration `divMod` 60 in T.pack $ "(" <> show mins <> ":" <> show secs <> ")"
data SChatType (c :: ChatType) where
SCTDirect :: SChatType 'CTDirect
@@ -548,11 +592,11 @@ instance TestEquality SChatType where
testEquality _ _ = Nothing
class ChatTypeI (c :: ChatType) where
- chatType :: SChatType c
+ chatTypeI :: SChatType c
-instance ChatTypeI 'CTDirect where chatType = SCTDirect
+instance ChatTypeI 'CTDirect where chatTypeI = SCTDirect
-instance ChatTypeI 'CTGroup where chatType = SCTGroup
+instance ChatTypeI 'CTGroup where chatTypeI = SCTGroup
data NewMessage = NewMessage
{ chatMsgEvent :: ChatMsgEvent,
diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs
index 928e4dca98..6a61f96691 100644
--- a/src/Simplex/Chat/Protocol.hs
+++ b/src/Simplex/Chat/Protocol.hs
@@ -29,6 +29,7 @@ import Data.Time.Clock (UTCTime)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic)
+import Simplex.Chat.Call
import Simplex.Chat.Types
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
import Simplex.Messaging.Encoding.String
@@ -132,6 +133,11 @@ data ChatMsgEvent
| XInfoProbe Probe
| XInfoProbeCheck ProbeHash
| XInfoProbeOk Probe
+ | XCallInv CallId CallInvitation
+ | XCallOffer CallId CallOffer
+ | XCallAnswer CallId CallAnswer
+ | XCallExtra CallId CallExtraInfo
+ | XCallEnd CallId
| XOk
| XUnknown {event :: Text, params :: J.Object}
deriving (Eq, Show)
@@ -306,6 +312,11 @@ data CMEventTag
| XInfoProbe_
| XInfoProbeCheck_
| XInfoProbeOk_
+ | XCallInv_
+ | XCallOffer_
+ | XCallAnswer_
+ | XCallExtra_
+ | XCallEnd_
| XOk_
| XUnknown_ Text
deriving (Eq, Show)
@@ -336,6 +347,11 @@ instance StrEncoding CMEventTag where
XInfoProbe_ -> "x.info.probe"
XInfoProbeCheck_ -> "x.info.probe.check"
XInfoProbeOk_ -> "x.info.probe.ok"
+ XCallInv_ -> "x.call.inv"
+ XCallOffer_ -> "x.call.offer"
+ XCallAnswer_ -> "x.call.answer"
+ XCallExtra_ -> "x.call.extra"
+ XCallEnd_ -> "x.call.end"
XOk_ -> "x.ok"
XUnknown_ t -> encodeUtf8 t
strDecode = \case
@@ -363,6 +379,11 @@ instance StrEncoding CMEventTag where
"x.info.probe" -> Right XInfoProbe_
"x.info.probe.check" -> Right XInfoProbeCheck_
"x.info.probe.ok" -> Right XInfoProbeOk_
+ "x.call.inv" -> Right XCallInv_
+ "x.call.offer" -> Right XCallOffer_
+ "x.call.answer" -> Right XCallAnswer_
+ "x.call.extra" -> Right XCallExtra_
+ "x.call.end" -> Right XCallEnd_
"x.ok" -> Right XOk_
t -> Right . XUnknown_ $ safeDecodeUtf8 t
strP = strDecode <$?> A.takeTill (== ' ')
@@ -393,6 +414,11 @@ toCMEventTag = \case
XInfoProbe _ -> XInfoProbe_
XInfoProbeCheck _ -> XInfoProbeCheck_
XInfoProbeOk _ -> XInfoProbeOk_
+ XCallInv _ _ -> XCallInv_
+ XCallOffer _ _ -> XCallOffer_
+ XCallAnswer _ _ -> XCallAnswer_
+ XCallExtra _ _ -> XCallExtra_
+ XCallEnd _ -> XCallEnd_
XOk -> XOk_
XUnknown t _ -> XUnknown_ t
@@ -441,6 +467,11 @@ appToChatMessage AppMessage {msgId, event, params} = do
XInfoProbe_ -> XInfoProbe <$> p "probe"
XInfoProbeCheck_ -> XInfoProbeCheck <$> p "probeHash"
XInfoProbeOk_ -> XInfoProbeOk <$> p "probe"
+ XCallInv_ -> XCallInv <$> p "callId" <*> p "invitation"
+ XCallOffer_ -> XCallOffer <$> p "callId" <*> p "offer"
+ XCallAnswer_ -> XCallAnswer <$> p "callId" <*> p "answer"
+ XCallExtra_ -> XCallExtra <$> p "callId" <*> p "extra"
+ XCallEnd_ -> XCallEnd <$> p "callId"
XOk_ -> pure XOk
XUnknown_ t -> pure $ XUnknown t params
@@ -476,6 +507,11 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = AppMessage {msgId, event, p
XInfoProbe probe -> o ["probe" .= probe]
XInfoProbeCheck probeHash -> o ["probeHash" .= probeHash]
XInfoProbeOk probe -> o ["probe" .= probe]
+ XCallInv callId inv -> o ["callId" .= callId, "invitation" .= inv]
+ XCallOffer callId offer -> o ["callId" .= callId, "offer" .= offer]
+ XCallAnswer callId answer -> o ["callId" .= callId, "answer" .= answer]
+ XCallExtra callId extra -> o ["callId" .= callId, "extra" .= extra]
+ XCallEnd callId -> o ["callId" .= callId]
XOk -> JM.empty
XUnknown _ ps -> ps
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index fe489e2c72..080c25b4f5 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -139,6 +139,11 @@ responseToView testView = \case
["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
CRRcvFileSubError RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e ->
["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
+ CRCallInvitation {} -> []
+ CRCallOffer {} -> []
+ CRCallAnswer {} -> []
+ CRCallExtraInfo {} -> []
+ CRCallEnded {} -> []
CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"]
CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"]
CRNewContactConnection _ -> []
@@ -185,11 +190,13 @@ viewChatItem chat ChatItem {chatDir, meta, content, quotedItem, file} = case cha
CIDirectSnd -> case content of
CISndMsgContent mc -> withSndFile to $ sndMsg to quote mc
CISndDeleted _ -> []
+ CISndCall {} -> []
where
to = ttyToContact' c
CIDirectRcv -> case content of
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
CIRcvDeleted _ -> []
+ CIRcvCall {} -> []
where
from = ttyFromContact' c
where
@@ -198,11 +205,13 @@ viewChatItem chat ChatItem {chatDir, meta, content, quotedItem, file} = case cha
CIGroupSnd -> case content of
CISndMsgContent mc -> withSndFile to $ sndMsg to quote mc
CISndDeleted _ -> []
+ CISndCall {} -> []
where
to = ttyToGroup g
CIGroupRcv m -> case content of
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
CIRcvDeleted _ -> []
+ CIRcvCall {} -> []
where
from = ttyFromGroup' g m
where