Merge branch 'master' into master-ios
This commit is contained in:
@@ -134,7 +134,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
||||
|
||||
// Returns real duration of the track
|
||||
private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? {
|
||||
val absoluteFilePath = getAppFilePath(fileSource.filePath)
|
||||
val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath)
|
||||
if (!File(absoluteFilePath).exists()) {
|
||||
Log.e(TAG, "No such file: ${fileSource.filePath}")
|
||||
return null
|
||||
@@ -272,10 +272,10 @@ actual object AudioPlayer: AudioPlayerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
override fun duration(filePath: String): Int? {
|
||||
override fun duration(unencryptedFilePath: String): Int? {
|
||||
var res: Int? = null
|
||||
kotlin.runCatching {
|
||||
helperPlayer.setDataSource(filePath)
|
||||
helperPlayer.setDataSource(unencryptedFilePath)
|
||||
helperPlayer.prepare()
|
||||
helperPlayer.start()
|
||||
helperPlayer.stop()
|
||||
|
||||
@@ -2089,6 +2089,10 @@ data class CryptoFile(
|
||||
val filePath: String,
|
||||
val cryptoArgs: CryptoFileArgs?
|
||||
) {
|
||||
|
||||
val isAbsolutePath: Boolean
|
||||
get() = File(filePath).isAbsolute
|
||||
|
||||
companion object {
|
||||
fun plain(f: String): CryptoFile = CryptoFile(f, null)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ interface AudioPlayerInterface {
|
||||
fun stop(fileName: String?)
|
||||
fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>)
|
||||
fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?)
|
||||
fun duration(filePath: String): Int?
|
||||
fun duration(unencryptedFilePath: String): Int?
|
||||
}
|
||||
|
||||
expect object AudioPlayer: AudioPlayerInterface
|
||||
|
||||
@@ -42,7 +42,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
||||
/*LALAL*/
|
||||
}
|
||||
|
||||
override fun duration(filePath: String): Int? {
|
||||
override fun duration(unencryptedFilePath: String): Int? {
|
||||
/*LALAL*/
|
||||
return null
|
||||
}
|
||||
|
||||
56
docs/rfcs/2023-09-12-group-member-contacts.md
Normal file
56
docs/rfcs/2023-09-12-group-member-contacts.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Groups member contacts
|
||||
|
||||
## Problem
|
||||
|
||||
Ability to send direct messages to group members, w/t creating additional direct connections, while keeping existing UX of separate conversations.
|
||||
|
||||
## Solution
|
||||
|
||||
### Protocol
|
||||
|
||||
Same changes on chat protocol level as for direct messages:
|
||||
|
||||
```haskell
|
||||
data MessageScope = MSGroup | MSDirect
|
||||
|
||||
-- ExtMsgContent extended with scope
|
||||
data ExtMsgContent = ExtMsgContent
|
||||
{ ...
|
||||
scope :: Maybe MessageScope
|
||||
}
|
||||
```
|
||||
|
||||
Changes to MsgRef are not necessary - it would be impossible to quote group message directly to member contact.
|
||||
|
||||
### Model
|
||||
|
||||
If member doesn't have existing contact (e.g. legacy, merged or "member contact" created before), button "Send direct message" / "Open direct chat" in UI would create a new "member contact" record in contacts table that would use the same connection as group member.
|
||||
|
||||
New API:
|
||||
|
||||
```haskell
|
||||
APICreateMemberContact GroupId GroupMemberId
|
||||
```
|
||||
|
||||
- would create a new contact record and assign contact_id to group_member record
|
||||
- should be unmergeable so that the same connection is not used for member across groups
|
||||
- flag
|
||||
- or group_member_id in contacts table
|
||||
- member removal (member leaves or removed) and deletion (group is deleted) should check if "member contact" exists for the same connection, if yes connection should be kept
|
||||
- contact deletion should also check if member exists
|
||||
- due to ON DELETE CASCADE constraints entity id should be first set to null before deleting contact/member record
|
||||
|
||||
On receiving group message with MSDirect scope, if "member contact" doesn't exist it should be created (same as above).
|
||||
|
||||
What if member record already had regular contact assigned? (a contact with a separate connection and no group_member_id) It can happen if merge completed for these members previously, and one of the members deleted contact; or if merge has only completed for one of members.
|
||||
|
||||
- Assigning chat items received as "member contact" to a regular contact would mix messages from different connections in the same conversation. This would practically be indistinguishable in UI, but would further complicate understanding of connection level errors.
|
||||
- Replacing contact_id for group_members record would seem to user as if previous messages were lost when chat is entered via "open direct chat" button, unless there's an indication inside this new chat that an old contact exists separately (requires yet additional field on contact - main_contact_id?).
|
||||
|
||||
When next messages are received read connection entity based on message scope? (parameterize getConnectionEntity, toConnection with message scope). Move message scope on a level above ExtMsgContent? If it's on AChatMsgEvent level - this would allow only MSG agent messages and not make it a fully fledged connection entity. Should "member contact" be able to receive any messages other than XMsgNew and messages referring by shared msg id? If not, maybe it shouldn't be treated as connection entity and instead "member contact" record should be read ad-hoc when processing these messages (for example see processGroupScopeMsg for group direct message). For example, should "member contacts" be available for inviting to other groups? Probably not - if they were, connection established in one group would be reused in another group.
|
||||
|
||||
We could also make distinction between regular contacts and "member contacts" in chat list - e.g. show both group and member avatar/icon, or include group name in conversation name. This would also improve clarity for users which contacts were created for which reason and to better understand consequences to deleting it.
|
||||
|
||||
TODO:
|
||||
|
||||
We should also double check that removed members messages are dropped if received - right now their connections are deleted so it's practically not possible for them to send after being removed; if their connection is kept for "member contact" purpose though, they would still be able to send group messages.
|
||||
@@ -111,7 +111,6 @@ library
|
||||
Simplex.Chat.Migrations.M20230827_file_encryption
|
||||
Simplex.Chat.Migrations.M20230829_connections_chat_vrange
|
||||
Simplex.Chat.Migrations.M20230903_connections_to_subscribe
|
||||
Simplex.Chat.Migrations.M20230904_item_direct_group_member_id
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.File
|
||||
Simplex.Chat.Mobile.Shared
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,7 +67,7 @@ sendComposedMessage cc = sendComposedMessage' cc . contactId'
|
||||
sendComposedMessage' :: ChatController -> ContactId -> Maybe ChatItemId -> MsgContent -> IO ()
|
||||
sendComposedMessage' cc ctId quotedItemId msgContent = do
|
||||
let cm = ComposedMessage {fileSource = Nothing, quotedItemId, msgContent}
|
||||
sendChatCmd cc (APISendMessage (SRDirect ctId) False Nothing cm) >>= \case
|
||||
sendChatCmd cc (APISendMessage (ChatRef CTDirect ctId) False Nothing cm) >>= \case
|
||||
CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId
|
||||
r -> putStrLn $ "unexpected send message response: " <> show r
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map.Strict (Map)
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time (NominalDiffTime)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Version (showVersion)
|
||||
@@ -242,7 +241,7 @@ data ChatCommand
|
||||
| APIGetChat ChatRef ChatPagination (Maybe String)
|
||||
| APIGetChatItems ChatPagination (Maybe String)
|
||||
| APIGetChatItemInfo ChatRef ChatItemId
|
||||
| APISendMessage {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessage :: ComposedMessage}
|
||||
| APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessage :: ComposedMessage}
|
||||
| APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, msgContent :: MsgContent}
|
||||
| APIDeleteChatItem ChatRef ChatItemId CIDeleteMode
|
||||
| APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId
|
||||
@@ -353,14 +352,14 @@ data ChatCommand
|
||||
| AddressAutoAccept (Maybe AutoAccept)
|
||||
| AcceptContact IncognitoEnabled ContactName
|
||||
| RejectContact ContactName
|
||||
| SendMessage SendName Text
|
||||
| SendLiveMessage SendName Text
|
||||
| SendMessage ChatName Text
|
||||
| SendLiveMessage ChatName Text
|
||||
| SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: Text, message :: Text}
|
||||
| SendMessageBroadcast Text -- UserId (not used in UI)
|
||||
| DeleteMessage ChatName Text
|
||||
| DeleteMemberMessage GroupName ContactName Text
|
||||
| EditMessage {chatName :: ChatName, editedMsg :: Text, message :: Text}
|
||||
| UpdateLiveMessage {sendName :: SendName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: Text}
|
||||
| UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: Text}
|
||||
| ReactToMessage {add :: Bool, reaction :: MsgReaction, chatName :: ChatName, reactToMessage :: Text}
|
||||
| APINewGroup UserId GroupProfile
|
||||
| NewGroup GroupProfile
|
||||
@@ -382,17 +381,17 @@ data ChatCommand
|
||||
| GroupLinkMemberRole GroupName GroupMemberRole
|
||||
| DeleteGroupLink GroupName
|
||||
| ShowGroupLink GroupName
|
||||
| SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, directMemberName :: Maybe ContactName, quotedMsg :: Text, message :: Text}
|
||||
| SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: Text, message :: Text}
|
||||
| LastChats (Maybe Int) -- UserId (not used in UI)
|
||||
| LastMessages (Maybe ChatName) Int (Maybe String) -- UserId (not used in UI)
|
||||
| LastChatItemId (Maybe ChatName) Int -- UserId (not used in UI)
|
||||
| ShowChatItem (Maybe ChatItemId) -- UserId (not used in UI)
|
||||
| ShowChatItemInfo ChatName Text
|
||||
| ShowLiveItems Bool
|
||||
| SendFile SendName FilePath
|
||||
| SendImage SendName FilePath
|
||||
| ForwardFile SendName FileTransferId
|
||||
| ForwardImage SendName FileTransferId
|
||||
| SendFile ChatName FilePath
|
||||
| SendImage ChatName FilePath
|
||||
| ForwardFile ChatName FileTransferId
|
||||
| ForwardImage ChatName FileTransferId
|
||||
| SendFileDescription ChatName FilePath
|
||||
| ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
|
||||
| SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Bool}
|
||||
@@ -613,37 +612,6 @@ instance ToJSON ChatResponse where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
|
||||
|
||||
data SendRef
|
||||
= SRDirect ContactId
|
||||
| SRGroup GroupId (Maybe GroupMemberId)
|
||||
deriving (Eq, Show)
|
||||
|
||||
sendToChatRef :: SendRef -> ChatRef
|
||||
sendToChatRef = \case
|
||||
SRDirect cId -> ChatRef CTDirect cId
|
||||
SRGroup gId _ -> ChatRef CTGroup gId
|
||||
|
||||
data SendName
|
||||
= SNDirect ContactName
|
||||
| SNGroup GroupName (Maybe ContactName)
|
||||
deriving (Eq, Show)
|
||||
|
||||
sendNameStr :: SendName -> String
|
||||
sendNameStr = \case
|
||||
SNDirect cName -> "@" <> T.unpack cName
|
||||
SNGroup gName (Just cName) -> "#" <> T.unpack gName <> " @" <> T.unpack cName
|
||||
SNGroup gName Nothing -> "#" <> T.unpack gName
|
||||
|
||||
data SendDirection
|
||||
= SDDirect Contact
|
||||
| SDGroup GroupInfo [GroupMember]
|
||||
deriving (Eq, Show)
|
||||
|
||||
sendDirToContactOrGroup :: SendDirection -> ContactOrGroup
|
||||
sendDirToContactOrGroup = \case
|
||||
SDDirect c -> CGContact c
|
||||
SDGroup g _ -> CGGroup g
|
||||
|
||||
newtype UserPwd = UserPwd {unUserPwd :: Text}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -959,7 +927,6 @@ data ChatErrorType
|
||||
| CEAgentCommandError {message :: String}
|
||||
| CEInvalidFileDescription {message :: String}
|
||||
| CEConnectionIncognitoChangeProhibited
|
||||
| CEPeerChatVRangeIncompatible
|
||||
| CEInternalError {message :: String}
|
||||
| CEException {message :: String}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
@@ -50,6 +50,16 @@ data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
|
||||
data ChatName = ChatName ChatType Text
|
||||
deriving (Show)
|
||||
|
||||
chatTypeStr :: ChatType -> String
|
||||
chatTypeStr = \case
|
||||
CTDirect -> "@"
|
||||
CTGroup -> "#"
|
||||
CTContactRequest -> "<@"
|
||||
CTContactConnection -> ":"
|
||||
|
||||
chatNameStr :: ChatName -> String
|
||||
chatNameStr (ChatName cType name) = chatTypeStr cType <> T.unpack name
|
||||
|
||||
data ChatRef = ChatRef ChatType Int64
|
||||
deriving (Eq, Show, Ord)
|
||||
|
||||
@@ -138,16 +148,16 @@ instance MsgDirectionI d => ToJSON (ChatItem c d) where
|
||||
data CIDirection (c :: ChatType) (d :: MsgDirection) where
|
||||
CIDirectSnd :: CIDirection 'CTDirect 'MDSnd
|
||||
CIDirectRcv :: CIDirection 'CTDirect 'MDRcv
|
||||
CIGroupSnd :: Maybe GroupMember -> CIDirection 'CTGroup 'MDSnd
|
||||
CIGroupRcv :: GroupMember -> MessageScope -> CIDirection 'CTGroup 'MDRcv
|
||||
CIGroupSnd :: CIDirection 'CTGroup 'MDSnd
|
||||
CIGroupRcv :: GroupMember -> CIDirection 'CTGroup 'MDRcv
|
||||
|
||||
deriving instance Show (CIDirection c d)
|
||||
|
||||
data JSONCIDirection
|
||||
= JCIDirectSnd
|
||||
| JCIDirectRcv
|
||||
| JCIGroupSnd {directMember :: Maybe GroupMember}
|
||||
| JCIGroupRcv {groupMember :: GroupMember, messageScope :: MessageScope}
|
||||
| JCIGroupSnd
|
||||
| JCIGroupRcv {groupMember :: GroupMember}
|
||||
deriving (Generic, Show)
|
||||
|
||||
instance ToJSON JSONCIDirection where
|
||||
@@ -162,19 +172,8 @@ jsonCIDirection :: CIDirection c d -> JSONCIDirection
|
||||
jsonCIDirection = \case
|
||||
CIDirectSnd -> JCIDirectSnd
|
||||
CIDirectRcv -> JCIDirectRcv
|
||||
CIGroupSnd dm -> JCIGroupSnd dm
|
||||
CIGroupRcv m ms -> JCIGroupRcv m ms
|
||||
|
||||
ciDirDirectMember :: CIDirection 'CTGroup d -> Maybe GroupMember
|
||||
ciDirDirectMember = \case
|
||||
CIGroupSnd dm -> dm
|
||||
CIGroupRcv _ MSGroup -> Nothing
|
||||
CIGroupRcv m MSDirect -> Just m
|
||||
|
||||
directMemberToMsgScope :: Maybe GroupMember -> MessageScope
|
||||
directMemberToMsgScope = \case
|
||||
Nothing -> MSGroup
|
||||
Just _ -> MSDirect
|
||||
CIGroupSnd -> JCIGroupSnd
|
||||
CIGroupRcv m -> JCIGroupRcv m
|
||||
|
||||
data CIReactionCount = CIReactionCount {reaction :: MsgReaction, userReacted :: Bool, totalReacted :: Int}
|
||||
deriving (Show, Generic)
|
||||
@@ -209,8 +208,8 @@ timedDeleteAt' CITimed {deleteAt} = deleteAt
|
||||
|
||||
chatItemMember :: GroupInfo -> ChatItem 'CTGroup d -> GroupMember
|
||||
chatItemMember GroupInfo {membership} ChatItem {chatDir} = case chatDir of
|
||||
CIGroupSnd _ -> membership
|
||||
CIGroupRcv m _ -> m
|
||||
CIGroupSnd -> membership
|
||||
CIGroupRcv m -> m
|
||||
|
||||
ciReactionAllowed :: ChatItem c d -> Bool
|
||||
ciReactionAllowed ChatItem {meta = CIMeta {itemDeleted = Just _}} = False
|
||||
@@ -239,22 +238,22 @@ chatItemDeletedState ChatItem {meta = CIMeta {itemDeleted}, content} =
|
||||
data ChatDirection (c :: ChatType) (d :: MsgDirection) where
|
||||
CDDirectSnd :: Contact -> ChatDirection 'CTDirect 'MDSnd
|
||||
CDDirectRcv :: Contact -> ChatDirection 'CTDirect 'MDRcv
|
||||
CDGroupSnd :: GroupInfo -> Maybe GroupMember -> ChatDirection 'CTGroup 'MDSnd
|
||||
CDGroupRcv :: GroupInfo -> GroupMember -> MessageScope -> ChatDirection 'CTGroup 'MDRcv
|
||||
CDGroupSnd :: GroupInfo -> ChatDirection 'CTGroup 'MDSnd
|
||||
CDGroupRcv :: GroupInfo -> GroupMember -> ChatDirection 'CTGroup 'MDRcv
|
||||
|
||||
toCIDirection :: ChatDirection c d -> CIDirection c d
|
||||
toCIDirection = \case
|
||||
CDDirectSnd _ -> CIDirectSnd
|
||||
CDDirectRcv _ -> CIDirectRcv
|
||||
CDGroupSnd _ dm -> CIGroupSnd dm
|
||||
CDGroupRcv _ m ms -> CIGroupRcv m ms
|
||||
CDGroupSnd _ -> CIGroupSnd
|
||||
CDGroupRcv _ m -> CIGroupRcv m
|
||||
|
||||
toChatInfo :: ChatDirection c d -> ChatInfo c
|
||||
toChatInfo = \case
|
||||
CDDirectSnd c -> DirectChat c
|
||||
CDDirectRcv c -> DirectChat c
|
||||
CDGroupSnd g _ -> GroupChat g
|
||||
CDGroupRcv g _ _ -> GroupChat g
|
||||
CDGroupSnd g -> GroupChat g
|
||||
CDGroupRcv g _ -> GroupChat g
|
||||
|
||||
data NewChatItem d = NewChatItem
|
||||
{ createdByMsgId :: Maybe MessageId,
|
||||
@@ -434,39 +433,29 @@ instance ToJSON (JSONCIReaction c d) where
|
||||
data CIQDirection (c :: ChatType) where
|
||||
CIQDirectSnd :: CIQDirection 'CTDirect
|
||||
CIQDirectRcv :: CIQDirection 'CTDirect
|
||||
CIQGroupSnd :: MessageScope -> CIQDirection 'CTGroup
|
||||
CIQGroupRcv :: Maybe GroupMember -> MessageScope -> CIQDirection 'CTGroup -- member can be Nothing in case MsgRef has memberId that the user is not notified about yet
|
||||
CIQGroupSnd :: CIQDirection 'CTGroup
|
||||
CIQGroupRcv :: Maybe GroupMember -> CIQDirection 'CTGroup -- member can be Nothing in case MsgRef has memberId that the user is not notified about yet
|
||||
|
||||
deriving instance Show (CIQDirection c)
|
||||
|
||||
data JSONCIQDirection
|
||||
= JCIQDirectSnd
|
||||
| JCIQDirectRcv
|
||||
| JCIQGroupSnd {messageScope :: MessageScope}
|
||||
| JCIQGroupRcv {groupMember :: Maybe GroupMember, messageScope :: MessageScope}
|
||||
deriving (Generic, Show)
|
||||
|
||||
instance ToJSON JSONCIQDirection where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCIQ"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCIQ"
|
||||
|
||||
instance ToJSON (CIQDirection c) where
|
||||
toJSON = J.toJSON . jsonCIQDirection
|
||||
toEncoding = J.toEncoding . jsonCIQDirection
|
||||
|
||||
jsonCIQDirection :: CIQDirection c -> JSONCIQDirection
|
||||
jsonCIQDirection :: CIQDirection c -> Maybe JSONCIDirection
|
||||
jsonCIQDirection = \case
|
||||
CIQDirectSnd -> JCIQDirectSnd
|
||||
CIQDirectRcv -> JCIQDirectRcv
|
||||
CIQGroupSnd ms -> JCIQGroupSnd ms
|
||||
CIQGroupRcv m ms -> JCIQGroupRcv m ms
|
||||
CIQDirectSnd -> Just JCIDirectSnd
|
||||
CIQDirectRcv -> Just JCIDirectRcv
|
||||
CIQGroupSnd -> Just JCIGroupSnd
|
||||
CIQGroupRcv (Just m) -> Just $ JCIGroupRcv m
|
||||
CIQGroupRcv Nothing -> Nothing
|
||||
|
||||
quoteMsgDirection :: CIQDirection c -> MsgDirection
|
||||
quoteMsgDirection = \case
|
||||
CIQDirectSnd -> MDSnd
|
||||
CIQDirectRcv -> MDRcv
|
||||
CIQGroupSnd _ -> MDSnd
|
||||
CIQGroupRcv _ _ -> MDRcv
|
||||
CIQGroupSnd -> MDSnd
|
||||
CIQGroupRcv _ -> MDRcv
|
||||
|
||||
data CIFile (d :: MsgDirection) = CIFile
|
||||
{ fileId :: Int64,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230904_item_direct_group_member_id where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20230904_item_direct_group_member_id :: Query
|
||||
m20230904_item_direct_group_member_id =
|
||||
[sql|
|
||||
ALTER TABLE chat_items ADD COLUMN item_direct_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL;
|
||||
ALTER TABLE chat_items ADD COLUMN quoted_message_scope TEXT;
|
||||
|
||||
CREATE INDEX idx_chat_items_item_direct_group_member_id ON chat_items(item_direct_group_member_id);
|
||||
|]
|
||||
|
||||
down_m20230904_item_direct_group_member_id :: Query
|
||||
down_m20230904_item_direct_group_member_id =
|
||||
[sql|
|
||||
DROP INDEX idx_chat_items_item_direct_group_member_id;
|
||||
|
||||
ALTER TABLE chat_items DROP COLUMN quoted_message_scope;
|
||||
ALTER TABLE chat_items DROP COLUMN item_direct_group_member_id;
|
||||
|]
|
||||
@@ -392,9 +392,7 @@ CREATE TABLE chat_items(
|
||||
timed_delete_at TEXT,
|
||||
item_live INTEGER,
|
||||
item_deleted_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL,
|
||||
item_deleted_ts TEXT,
|
||||
item_direct_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL,
|
||||
quoted_message_scope TEXT
|
||||
item_deleted_ts TEXT
|
||||
);
|
||||
CREATE TABLE chat_item_messages(
|
||||
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
|
||||
@@ -715,6 +713,3 @@ CREATE INDEX idx_chat_items_user_id_item_status ON chat_items(
|
||||
item_status
|
||||
);
|
||||
CREATE INDEX idx_connections_to_subscribe ON connections(to_subscribe);
|
||||
CREATE INDEX idx_chat_items_item_direct_group_member_id ON chat_items(
|
||||
item_direct_group_member_id
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Util
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||
import Simplex.Messaging.Version hiding (version)
|
||||
|
||||
@@ -58,10 +58,6 @@ supportedChatVRange = mkVersionRange 1 currentChatVersion
|
||||
groupNoDirectVRange :: VersionRange
|
||||
groupNoDirectVRange = mkVersionRange 2 currentChatVersion
|
||||
|
||||
-- version range that supports private messages from members in a group
|
||||
groupPrivateMessagesVRange :: VersionRange
|
||||
groupPrivateMessagesVRange = mkVersionRange 2 currentChatVersion
|
||||
|
||||
data ConnectionEntity
|
||||
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
||||
| RcvGroupMsgConnection {entityConnection :: Connection, groupInfo :: GroupInfo, groupMember :: GroupMember}
|
||||
@@ -162,28 +158,11 @@ instance ToJSON SharedMsgId where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
data MessageScope = MSGroup | MSDirect
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance FromJSON MessageScope where
|
||||
parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "MS"
|
||||
|
||||
instance ToJSON MessageScope where
|
||||
toJSON = J.genericToJSON . enumJSON $ dropPrefix "MS"
|
||||
toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "MS"
|
||||
|
||||
instance ToField MessageScope where
|
||||
toField = toField . encodeJSON
|
||||
|
||||
instance FromField MessageScope where
|
||||
fromField = fromTextField_ decodeJSON
|
||||
|
||||
data MsgRef = MsgRef
|
||||
{ msgId :: Maybe SharedMsgId,
|
||||
sentAt :: UTCTime,
|
||||
sent :: Bool,
|
||||
memberId :: Maybe MemberId, -- must be present in all group message references, both referencing sent and received
|
||||
msgScope :: Maybe MessageScope
|
||||
memberId :: Maybe MemberId -- must be present in all group message references, both referencing sent and received
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
@@ -468,13 +447,7 @@ msgContentTag = \case
|
||||
MCFile {} -> MCFile_
|
||||
MCUnknown {tag} -> MCUnknown_ tag
|
||||
|
||||
data ExtMsgContent = ExtMsgContent
|
||||
{ content :: MsgContent,
|
||||
file :: Maybe FileInvitation,
|
||||
ttl :: Maybe Int,
|
||||
live :: Maybe Bool,
|
||||
scope :: Maybe MessageScope
|
||||
}
|
||||
data ExtMsgContent = ExtMsgContent {content :: MsgContent, file :: Maybe FileInvitation, ttl :: Maybe Int, live :: Maybe Bool}
|
||||
deriving (Eq, Show)
|
||||
|
||||
parseMsgContainer :: J.Object -> JT.Parser MsgContainer
|
||||
@@ -483,10 +456,10 @@ parseMsgContainer v =
|
||||
<|> (v .: "forward" >>= \f -> (if f then MCForward else MCSimple) <$> mc)
|
||||
<|> MCSimple <$> mc
|
||||
where
|
||||
mc = ExtMsgContent <$> v .: "content" <*> v .:? "file" <*> v .:? "ttl" <*> v .:? "live" <*> v .:? "scope"
|
||||
mc = ExtMsgContent <$> v .: "content" <*> v .:? "file" <*> v .:? "ttl" <*> v .:? "live"
|
||||
|
||||
extMsgContent :: MsgContent -> Maybe FileInvitation -> ExtMsgContent
|
||||
extMsgContent mc file = ExtMsgContent mc file Nothing Nothing Nothing
|
||||
extMsgContent mc file = ExtMsgContent mc file Nothing Nothing
|
||||
|
||||
justTrue :: Bool -> Maybe Bool
|
||||
justTrue True = Just True
|
||||
@@ -530,7 +503,7 @@ msgContainerJSON = \case
|
||||
MCSimple mc -> o $ msgContent mc
|
||||
where
|
||||
o = JM.fromList
|
||||
msgContent (ExtMsgContent c file ttl live scope) = ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("scope" .=? scope) ["content" .= c]
|
||||
msgContent (ExtMsgContent c file ttl live) = ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) ["content" .= c]
|
||||
|
||||
instance ToJSON MsgContent where
|
||||
toJSON = \case
|
||||
|
||||
@@ -620,7 +620,7 @@ createNewContactMember db gVar User {userId, userContactId} groupId Contact {con
|
||||
createWithRandomId gVar $ \memId -> do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
|
||||
void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt subMode
|
||||
void $ createMemberConnection_ db userId groupMemberId agentConnId (fromJVersionRange peerChatVRange) Nothing 0 createdAt subMode
|
||||
pure member
|
||||
where
|
||||
createMember_ memberId createdAt = do
|
||||
|
||||
@@ -25,7 +25,6 @@ module Simplex.Chat.Store.Messages
|
||||
createRcvMsgDeliveryEvent,
|
||||
createPendingGroupMessage,
|
||||
getPendingGroupMessages,
|
||||
deleteMessage,
|
||||
deletePendingGroupMessage,
|
||||
deleteOldMessages,
|
||||
updateChatTs,
|
||||
@@ -290,10 +289,6 @@ getPendingGroupMessages db groupMemberId =
|
||||
pendingGroupMessage (msgId, cmEventTag, msgBody, introId_) =
|
||||
PendingGroupMessage {msgId, cmEventTag, msgBody, introId_}
|
||||
|
||||
deleteMessage :: DB.Connection -> MessageId -> IO ()
|
||||
deleteMessage db msgId = do
|
||||
DB.execute db "DELETE FROM messages WHERE message_id = ?" (Only msgId)
|
||||
|
||||
deletePendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO ()
|
||||
deletePendingGroupMessage db groupMemberId messageId =
|
||||
DB.execute db "DELETE FROM pending_group_messages WHERE group_member_id = ? AND message_id = ?" (groupMemberId, messageId)
|
||||
@@ -302,7 +297,7 @@ deleteOldMessages :: DB.Connection -> UTCTime -> IO ()
|
||||
deleteOldMessages db createdAtCutoff = do
|
||||
DB.execute db "DELETE FROM messages WHERE created_at <= ?" (Only createdAtCutoff)
|
||||
|
||||
type NewQuoteRow = (Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool, Maybe MemberId, Maybe MessageScope)
|
||||
type NewQuoteRow = (Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool, Maybe MemberId)
|
||||
|
||||
updateChatTs :: DB.Connection -> User -> ChatDirection c d -> UTCTime -> IO ()
|
||||
updateChatTs db User {userId} chatDirection chatTs = case toChatInfo chatDirection of
|
||||
@@ -325,15 +320,14 @@ createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciCon
|
||||
createdByMsgId = if msgId == 0 then Nothing else Just msgId
|
||||
quoteRow :: NewQuoteRow
|
||||
quoteRow = case quotedItem of
|
||||
Nothing -> (Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
|
||||
Just CIQuote {chatDir, sharedMsgId = quotedSharedMsgId, sentAt, content} -> do
|
||||
let (quotedSent, quotedMemberId, quotedMsgScope) = case chatDir of
|
||||
CIQDirectSnd -> (Just True, Nothing, Nothing)
|
||||
CIQDirectRcv -> (Just False, Nothing, Nothing)
|
||||
CIQGroupSnd messageScope -> (Just True, Nothing, Just messageScope)
|
||||
CIQGroupRcv (Just GroupMember {memberId}) messageScope -> (Just False, Just memberId, Just messageScope)
|
||||
CIQGroupRcv Nothing messageScope -> (Just False, Nothing, Just messageScope)
|
||||
(quotedSharedMsgId, Just sentAt, Just content, quotedSent, quotedMemberId, quotedMsgScope)
|
||||
Nothing -> (Nothing, Nothing, Nothing, Nothing, Nothing)
|
||||
Just CIQuote {chatDir, sharedMsgId = quotedSharedMsgId, sentAt, content} ->
|
||||
uncurry (quotedSharedMsgId,Just sentAt,Just content,,) $ case chatDir of
|
||||
CIQDirectSnd -> (Just True, Nothing)
|
||||
CIQDirectRcv -> (Just False, Nothing)
|
||||
CIQGroupSnd -> (Just True, Nothing)
|
||||
CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId)
|
||||
CIQGroupRcv Nothing -> (Just False, Nothing)
|
||||
|
||||
createNewRcvChatItem :: DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c))
|
||||
createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent} sharedMsgId_ ciContent timed live itemTs createdAt = do
|
||||
@@ -344,20 +338,19 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent} shar
|
||||
quotedMsg = cmToQuotedMsg chatMsgEvent
|
||||
quoteRow :: NewQuoteRow
|
||||
quoteRow = case quotedMsg of
|
||||
Nothing -> (Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
|
||||
Just QuotedMsg {msgRef = MsgRef {msgId = sharedMsgId, sentAt, sent, memberId, msgScope}, content} -> do
|
||||
let (quotedSent, quotedMemberId, quotedMsgScope) = case chatDirection of
|
||||
CDDirectRcv _ -> (Just $ not sent, Nothing, Nothing)
|
||||
CDGroupRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ _ ->
|
||||
(Just $ Just userMemberId == memberId, memberId, msgScope)
|
||||
(sharedMsgId, Just sentAt, Just content, quotedSent, quotedMemberId, quotedMsgScope)
|
||||
Nothing -> (Nothing, Nothing, Nothing, Nothing, Nothing)
|
||||
Just QuotedMsg {msgRef = MsgRef {msgId = sharedMsgId, sentAt, sent, memberId}, content} ->
|
||||
uncurry (sharedMsgId,Just sentAt,Just content,,) $ case chatDirection of
|
||||
CDDirectRcv _ -> (Just $ not sent, Nothing)
|
||||
CDGroupRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ ->
|
||||
(Just $ Just userMemberId == memberId, memberId)
|
||||
|
||||
createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> IO ChatItemId
|
||||
createNewChatItemNoMsg db user chatDirection ciContent =
|
||||
createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow Nothing False
|
||||
where
|
||||
quoteRow :: NewQuoteRow
|
||||
quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing, Nothing)
|
||||
quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing)
|
||||
|
||||
createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO ChatItemId
|
||||
createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent quoteRow timed live itemTs createdAt = do
|
||||
@@ -366,12 +359,12 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q
|
||||
[sql|
|
||||
INSERT INTO chat_items (
|
||||
-- user and IDs
|
||||
user_id, created_by_msg_id, contact_id, group_id, group_member_id, item_direct_group_member_id,
|
||||
user_id, created_by_msg_id, contact_id, group_id, group_member_id,
|
||||
-- meta
|
||||
item_sent, item_ts, item_content, item_text, item_status, shared_msg_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at,
|
||||
-- quote
|
||||
quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, quoted_message_scope
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
((userId, msgId_) :. idsRow :. itemRow :. quoteRow)
|
||||
ciId <- insertedRowId db
|
||||
@@ -380,16 +373,12 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q
|
||||
where
|
||||
itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, CIStatus d, Maybe SharedMsgId) :. (UTCTime, UTCTime, Maybe Bool) :. (Maybe Int, Maybe UTCTime)
|
||||
itemRow = (msgDirection @d, itemTs, ciContent, ciContentToText ciContent, ciCreateStatus ciContent, sharedMsgId) :. (createdAt, createdAt, justTrue live) :. ciTimedRow timed
|
||||
idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
|
||||
idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64)
|
||||
idsRow = case chatDirection of
|
||||
CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing, Nothing)
|
||||
CDDirectSnd Contact {contactId} -> (Just contactId, Nothing, Nothing, Nothing)
|
||||
CDGroupRcv GroupInfo {groupId} GroupMember {groupMemberId} messageScope -> case messageScope of
|
||||
MSGroup -> (Nothing, Just groupId, Just groupMemberId, Nothing)
|
||||
MSDirect -> (Nothing, Just groupId, Just groupMemberId, Just groupMemberId)
|
||||
CDGroupSnd GroupInfo {groupId} directMember -> case directMember of
|
||||
Nothing -> (Nothing, Just groupId, Nothing, Nothing)
|
||||
Just GroupMember {groupMemberId} -> (Nothing, Just groupId, Nothing, Just groupMemberId)
|
||||
CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing)
|
||||
CDDirectSnd Contact {contactId} -> (Just contactId, Nothing, Nothing)
|
||||
CDGroupRcv GroupInfo {groupId} GroupMember {groupMemberId} -> (Nothing, Just groupId, Just groupMemberId)
|
||||
CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing)
|
||||
|
||||
ciTimedRow :: Maybe CITimed -> (Maybe Int, Maybe UTCTime)
|
||||
ciTimedRow (Just CITimed {ttl, deleteAt}) = (Just ttl, deleteAt)
|
||||
@@ -399,21 +388,19 @@ insertChatItemMessage_ :: DB.Connection -> ChatItemId -> MessageId -> UTCTime ->
|
||||
insertChatItemMessage_ db ciId msgId ts = DB.execute db "INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?)" (ciId, msgId, ts, ts)
|
||||
|
||||
getChatItemQuote_ :: DB.Connection -> User -> ChatDirection c 'MDRcv -> QuotedMsg -> IO (CIQuote c)
|
||||
getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRef = MsgRef {msgId, sentAt, sent, memberId, msgScope}, content} =
|
||||
getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRef = MsgRef {msgId, sentAt, sent, memberId}, content} =
|
||||
case chatDirection of
|
||||
CDDirectRcv Contact {contactId} -> getDirectChatItemQuote_ contactId (not sent)
|
||||
CDGroupRcv GroupInfo {groupId, membership = GroupMember {memberId = userMemberId}} sender@GroupMember {memberId = senderMemberId} _directMember ->
|
||||
CDGroupRcv GroupInfo {groupId, membership = GroupMember {memberId = userMemberId}} sender@GroupMember {memberId = senderMemberId} ->
|
||||
case memberId of
|
||||
Just mId
|
||||
| mId == userMemberId -> (`ciQuote` CIQGroupSnd messageScope) <$> getUserGroupChatItemId_ groupId
|
||||
| mId == senderMemberId -> (`ciQuote` CIQGroupRcv (Just sender) messageScope) <$> getGroupChatItemId_ groupId mId
|
||||
| mId == userMemberId -> (`ciQuote` CIQGroupSnd) <$> getUserGroupChatItemId_ groupId
|
||||
| mId == senderMemberId -> (`ciQuote` CIQGroupRcv (Just sender)) <$> getGroupChatItemId_ groupId mId
|
||||
| otherwise -> getGroupChatItemQuote_ groupId mId
|
||||
_ -> pure . ciQuote Nothing $ CIQGroupRcv Nothing messageScope
|
||||
_ -> pure . ciQuote Nothing $ CIQGroupRcv Nothing
|
||||
where
|
||||
ciQuote :: Maybe ChatItemId -> CIQDirection c -> CIQuote c
|
||||
ciQuote itemId dir = CIQuote dir itemId msgId sentAt content . parseMaybeMarkdownList $ msgContentText content
|
||||
messageScope :: MessageScope
|
||||
messageScope = fromMaybe MSGroup msgScope
|
||||
getDirectChatItemQuote_ :: Int64 -> Bool -> IO (CIQuote 'CTDirect)
|
||||
getDirectChatItemQuote_ contactId userSent = do
|
||||
fmap ciQuoteDirect . maybeFirstRow fromOnly $
|
||||
@@ -460,8 +447,8 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
|
||||
[":user_id" := userId, ":group_id" := groupId, ":member_id" := mId, ":msg_id" := msgId]
|
||||
where
|
||||
ciQuoteGroup :: [Only (Maybe ChatItemId) :. GroupMemberRow] -> CIQuote 'CTGroup
|
||||
ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing messageScope
|
||||
ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId $ CIQGroupRcv (Just $ toGroupMember userContactId memberRow) messageScope
|
||||
ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing
|
||||
ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow
|
||||
|
||||
getChatPreviews :: DB.Connection -> User -> Bool -> IO [AChat]
|
||||
getChatPreviews db user withPCC = do
|
||||
@@ -569,7 +556,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
|
||||
p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||
-- quoted ChatItem
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, i.quoted_message_scope,
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent,
|
||||
-- quoted GroupMember
|
||||
rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category,
|
||||
rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
|
||||
@@ -577,11 +564,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
-- deleted by GroupMember
|
||||
dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.member_role, dbm.member_category,
|
||||
dbm.member_status, dbm.invited_by, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
|
||||
dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences,
|
||||
-- direct GroupMember
|
||||
dirm.group_member_id, dirm.group_id, dirm.member_id, dirm.member_role, dirm.member_category,
|
||||
dirm.member_status, dirm.invited_by, dirm.local_display_name, dirm.contact_id, dirm.contact_profile_id, dirp.contact_profile_id,
|
||||
dirp.display_name, dirp.full_name, dirp.image, dirp.contact_link, dirp.local_alias, dirp.preferences
|
||||
dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences
|
||||
FROM groups g
|
||||
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
|
||||
JOIN group_members mu ON mu.group_id = g.group_id
|
||||
@@ -607,8 +590,6 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
LEFT JOIN contact_profiles rp ON rp.contact_profile_id = COALESCE(rm.member_profile_id, rm.contact_profile_id)
|
||||
LEFT JOIN group_members dbm ON dbm.group_member_id = i.item_deleted_by_group_member_id
|
||||
LEFT JOIN contact_profiles dbp ON dbp.contact_profile_id = COALESCE(dbm.member_profile_id, dbm.contact_profile_id)
|
||||
LEFT JOIN group_members dirm ON dirm.group_member_id = i.item_direct_group_member_id
|
||||
LEFT JOIN contact_profiles dirp ON dirp.contact_profile_id = COALESCE(dirm.member_profile_id, dirm.contact_profile_id)
|
||||
WHERE g.user_id = ? AND mu.contact_id = ?
|
||||
ORDER BY i.item_ts DESC
|
||||
|]
|
||||
@@ -986,8 +967,10 @@ toDirectQuote :: QuoteRow -> Maybe (CIQuote 'CTDirect)
|
||||
toDirectQuote qr@(_, _, _, _, quotedSent) = toQuote qr $ direction <$> quotedSent
|
||||
where
|
||||
direction sent = if sent then CIQDirectSnd else CIQDirectRcv
|
||||
toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir =
|
||||
CIQuote <$> dir <*> pure quotedItemId <*> pure quotedSharedMsgId <*> quotedSentAt <*> quotedMsgContent <*> (parseMaybeMarkdownList . msgContentText <$> quotedMsgContent)
|
||||
|
||||
toQuote :: QuoteRow -> Maybe (CIQDirection c) -> Maybe (CIQuote c)
|
||||
toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir =
|
||||
CIQuote <$> dir <*> pure quotedItemId <*> pure quotedSharedMsgId <*> quotedSentAt <*> quotedMsgContent <*> (parseMaybeMarkdownList . msgContentText <$> quotedMsgContent)
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
@@ -1030,60 +1013,37 @@ toDirectChatItemList currentTs (((Just itemId, Just itemTs, Just msgDir, Just it
|
||||
either (const []) (: []) $ toDirectChatItem currentTs (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. quoteRow)
|
||||
toDirectChatItemList _ _ = []
|
||||
|
||||
type GroupQuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool, Maybe MessageScope)
|
||||
type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow
|
||||
|
||||
type GroupQuoteMemberRow = GroupQuoteRow :. MaybeGroupMemberRow
|
||||
type MaybeGroupChatItemRow = MaybeChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow
|
||||
|
||||
type MaybeGroupChatItemRow = MaybeChatItemRow :. MaybeGroupMemberRow :. GroupQuoteMemberRow :. MaybeGroupMemberRow :. MaybeGroupMemberRow
|
||||
|
||||
toGroupQuote :: GroupQuoteRow -> Maybe GroupMember -> Maybe (CIQuote 'CTGroup)
|
||||
toGroupQuote qr@(_, _, _, _, quotedSent, msgScope) quotedMember_ =
|
||||
toQuote qr $ direction quotedSent quotedMember_
|
||||
toGroupQuote :: QuoteRow -> Maybe GroupMember -> Maybe (CIQuote 'CTGroup)
|
||||
toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction quotedSent quotedMember_
|
||||
where
|
||||
direction (Just True) _ = Just $ CIQGroupSnd messageScope
|
||||
direction (Just False) (Just member) = Just $ CIQGroupRcv (Just member) messageScope
|
||||
direction (Just False) Nothing = Just $ CIQGroupRcv Nothing messageScope
|
||||
direction (Just True) _ = Just CIQGroupSnd
|
||||
direction (Just False) (Just member) = Just . CIQGroupRcv $ Just member
|
||||
direction (Just False) Nothing = Just $ CIQGroupRcv Nothing
|
||||
direction _ _ = Nothing
|
||||
messageScope = fromMaybe MSGroup msgScope
|
||||
toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _, _) dir =
|
||||
CIQuote <$> dir <*> pure quotedItemId <*> pure quotedSharedMsgId <*> quotedSentAt <*> quotedMsgContent <*> (parseMaybeMarkdownList . msgContentText <$> quotedMsgContent)
|
||||
|
||||
-- this function can be changed so it never fails, not only avoid failure on invalid json
|
||||
toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteMemberRow :. MaybeGroupMemberRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
|
||||
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_ :. directMemberRow_) = do
|
||||
toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
|
||||
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
|
||||
chatItem $ fromRight invalid $ dbParseACIContent itemContentText
|
||||
where
|
||||
member_ = toMaybeGroupMember userContactId memberRow_
|
||||
quotedMember_ = toMaybeGroupMember userContactId quotedMemberRow_
|
||||
deletedByGroupMember_ = toMaybeGroupMember userContactId deletedByGroupMemberRow_
|
||||
directMember_ = toMaybeGroupMember userContactId directMemberRow_
|
||||
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
|
||||
chatItem itemContent = case (itemContent, itemStatus, member_, fileStatus_) of
|
||||
(ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, _, Just (AFS SMDSnd fileStatus)) ->
|
||||
Right $ cItem SMDSnd (CIGroupSnd directMember_) ciStatus ciContent (maybeCIFile fileStatus)
|
||||
Right $ cItem SMDSnd CIGroupSnd ciStatus ciContent (maybeCIFile fileStatus)
|
||||
(ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, _, Nothing) ->
|
||||
Right $ cItem SMDSnd (CIGroupSnd directMember_) ciStatus ciContent Nothing
|
||||
-- read of group chat item can be refactored so that direct member is not read for rcv items:
|
||||
-- if item_direct_group_member_id is equal to group_member_id, then message scope is direct
|
||||
Right $ cItem SMDSnd CIGroupSnd ciStatus ciContent Nothing
|
||||
(ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus, Just member, Just (AFS SMDRcv fileStatus)) ->
|
||||
case directMember_ of
|
||||
Just directMember
|
||||
| sameMember member directMember ->
|
||||
Right $ cItem SMDRcv (CIGroupRcv member MSDirect) ciStatus ciContent (maybeCIFile fileStatus)
|
||||
| otherwise -> badItem
|
||||
Nothing ->
|
||||
Right $ cItem SMDRcv (CIGroupRcv member MSGroup) ciStatus ciContent (maybeCIFile fileStatus)
|
||||
Right $ cItem SMDRcv (CIGroupRcv member) ciStatus ciContent (maybeCIFile fileStatus)
|
||||
(ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus, Just member, Nothing) ->
|
||||
case directMember_ of
|
||||
Just directMember
|
||||
| sameMember member directMember ->
|
||||
Right $ cItem SMDRcv (CIGroupRcv member MSDirect) ciStatus ciContent Nothing
|
||||
| otherwise -> badItem
|
||||
Nothing ->
|
||||
Right $ cItem SMDRcv (CIGroupRcv member MSGroup) ciStatus ciContent Nothing
|
||||
Right $ cItem SMDRcv (CIGroupRcv member) ciStatus ciContent Nothing
|
||||
_ -> badItem
|
||||
sameMember :: GroupMember -> GroupMember -> Bool
|
||||
sameMember GroupMember {groupMemberId = gmId1} GroupMember {groupMemberId = gmId2} = gmId1 == gmId2
|
||||
maybeCIFile :: CIFileStatus d -> Maybe (CIFile d)
|
||||
maybeCIFile fileStatus =
|
||||
case (fileId_, fileName_, fileSize_, fileProtocol_) of
|
||||
@@ -1108,8 +1068,8 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
|
||||
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
|
||||
|
||||
toGroupChatItemList :: UTCTime -> Int64 -> MaybeGroupChatItemRow -> [CChatItem 'CTGroup]
|
||||
toGroupChatItemList currentTs userContactId (((Just itemId, Just itemTs, Just msgDir, Just itemContent, Just itemText, Just itemStatus, sharedMsgId) :. (Just itemDeleted, deletedTs, itemEdited, Just createdAt, Just updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_ :. directMemberRow_) =
|
||||
either (const []) (: []) $ toGroupChatItem currentTs userContactId (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_ :. directMemberRow_)
|
||||
toGroupChatItemList currentTs userContactId (((Just itemId, Just itemTs, Just msgDir, Just itemContent, Just itemText, Just itemStatus, sharedMsgId) :. (Just itemDeleted, deletedTs, itemEdited, Just createdAt, Just updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) =
|
||||
either (const []) (: []) $ toGroupChatItem currentTs userContactId (((itemId, itemTs, msgDir, itemContent, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_)
|
||||
toGroupChatItemList _ _ _ = []
|
||||
|
||||
getAllChatItems :: DB.Connection -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem]
|
||||
@@ -1524,7 +1484,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
|
||||
p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||
-- quoted ChatItem
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, i.quoted_message_scope,
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent,
|
||||
-- quoted GroupMember
|
||||
rm.group_member_id, rm.group_id, rm.member_id, rm.member_role, rm.member_category,
|
||||
rm.member_status, rm.invited_by, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
|
||||
@@ -1532,11 +1492,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
-- deleted by GroupMember
|
||||
dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.member_role, dbm.member_category,
|
||||
dbm.member_status, dbm.invited_by, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
|
||||
dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences,
|
||||
-- direct GroupMember
|
||||
dirm.group_member_id, dirm.group_id, dirm.member_id, dirm.member_role, dirm.member_category,
|
||||
dirm.member_status, dirm.invited_by, dirm.local_display_name, dirm.contact_id, dirm.contact_profile_id, dirp.contact_profile_id,
|
||||
dirp.display_name, dirp.full_name, dirp.image, dirp.contact_link, dirp.local_alias, dirp.preferences
|
||||
dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences
|
||||
FROM chat_items i
|
||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
LEFT JOIN group_members m ON m.group_member_id = i.group_member_id
|
||||
@@ -1546,8 +1502,6 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
LEFT JOIN contact_profiles rp ON rp.contact_profile_id = COALESCE(rm.member_profile_id, rm.contact_profile_id)
|
||||
LEFT JOIN group_members dbm ON dbm.group_member_id = i.item_deleted_by_group_member_id
|
||||
LEFT JOIN contact_profiles dbp ON dbp.contact_profile_id = COALESCE(dbm.member_profile_id, dbm.contact_profile_id)
|
||||
LEFT JOIN group_members dirm ON dirm.group_member_id = i.item_direct_group_member_id
|
||||
LEFT JOIN contact_profiles dirp ON dirp.contact_profile_id = COALESCE(dirm.member_profile_id, dirm.contact_profile_id)
|
||||
WHERE i.user_id = ? AND i.group_id = ? AND i.chat_item_id = ?
|
||||
|]
|
||||
(userId, groupId, itemId)
|
||||
|
||||
@@ -79,7 +79,6 @@ import Simplex.Chat.Migrations.M20230814_indexes
|
||||
import Simplex.Chat.Migrations.M20230827_file_encryption
|
||||
import Simplex.Chat.Migrations.M20230829_connections_chat_vrange
|
||||
import Simplex.Chat.Migrations.M20230903_connections_to_subscribe
|
||||
import Simplex.Chat.Migrations.M20230904_item_direct_group_member_id
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -158,8 +157,7 @@ schemaMigrations =
|
||||
("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes),
|
||||
("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption),
|
||||
("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange),
|
||||
("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe),
|
||||
("20230904_item_direct_group_member_id", m20230904_item_direct_group_member_id, Just down_m20230904_item_direct_group_member_id)
|
||||
("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -144,7 +144,7 @@ toConnection :: ConnectionRow -> Connection
|
||||
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) =
|
||||
let entityId = entityId_ connType
|
||||
connectionCode = SecurityCode <$> code_ <*> verifiedAt_
|
||||
peerChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||
peerChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||
in Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
|
||||
where
|
||||
entityId_ :: ConnType -> Maybe Int64
|
||||
@@ -178,7 +178,7 @@ createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange
|
||||
:. (minV, maxV, subMode == SMOnlyCreate)
|
||||
)
|
||||
connId <- insertedRowId db
|
||||
pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
|
||||
pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange = JVersionRange peerChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
|
||||
where
|
||||
ent ct = if connType == ct then entityId else Nothing
|
||||
|
||||
@@ -279,7 +279,7 @@ type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, In
|
||||
toContactRequest :: ContactRequestRow -> UserContactRequest
|
||||
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt, minVer, maxVer)) = do
|
||||
let profile = Profile {displayName, fullName, image, contactLink, preferences}
|
||||
cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||
cReqChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||
in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt}
|
||||
|
||||
userQuery :: Query
|
||||
|
||||
@@ -73,19 +73,19 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
|
||||
Right SendMessageBroadcast {} -> True
|
||||
_ -> False
|
||||
startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO ()
|
||||
startLiveMessage (Right (SendLiveMessage sendName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do
|
||||
startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do
|
||||
whenM (isNothing <$> readTVarIO liveMessageState) $ do
|
||||
let s = T.unpack msg
|
||||
int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int
|
||||
liveThreadId <- mkWeakThreadId =<< runLiveMessage int `forkFinally` const (atomically $ writeTVar liveMessageState Nothing)
|
||||
promptThreadId <- mkWeakThreadId =<< forkIO blinkLivePrompt
|
||||
atomically $ do
|
||||
let lm = LiveMessage {sendName, chatItemId = itemId, livePrompt = True, sentMsg = s, typedMsg = s, liveThreadId, promptThreadId}
|
||||
let lm = LiveMessage {chatName, chatItemId = itemId, livePrompt = True, sentMsg = s, typedMsg = s, liveThreadId, promptThreadId}
|
||||
writeTVar liveMessageState (Just lm)
|
||||
modifyTVar termState $ \ts -> ts {inputString = s, inputPosition = length s, inputPrompt = liveInputPrompt lm}
|
||||
where
|
||||
liveInputPrompt LiveMessage {sendName = n, livePrompt} =
|
||||
"> " <> sendNameStr n <> " [" <> (if livePrompt then "LIVE" else " ") <> "] "
|
||||
liveInputPrompt LiveMessage {chatName = n, livePrompt} =
|
||||
"> " <> chatNameStr n <> " [" <> (if livePrompt then "LIVE" else " ") <> "] "
|
||||
runLiveMessage :: Int -> IO ()
|
||||
runLiveMessage int = do
|
||||
threadDelay int
|
||||
@@ -123,8 +123,8 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
|
||||
startLiveMessage _ _ = pure ()
|
||||
|
||||
sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO ChatResponse
|
||||
sendUpdatedLiveMessage cc sentMsg LiveMessage {sendName, chatItemId} live = do
|
||||
let cmd = UpdateLiveMessage sendName chatItemId live $ T.pack sentMsg
|
||||
sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do
|
||||
let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg
|
||||
either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc
|
||||
|
||||
runTerminalInput :: ChatTerminal -> ChatController -> IO ()
|
||||
@@ -174,14 +174,14 @@ receiveFromTTY cc@ChatController {inputQ, activeTo, currentUser, chatStore} ct@C
|
||||
let s = inputString ts
|
||||
lm_ <- readTVar liveMessageState
|
||||
case lm_ of
|
||||
Just LiveMessage {sendName}
|
||||
Just LiveMessage {chatName}
|
||||
| live -> do
|
||||
writeTVar termState ts' {previousInput}
|
||||
writeTBQueue inputQ $ "/live " <> sendNameStr sendName
|
||||
writeTBQueue inputQ $ "/live " <> chatNameStr chatName
|
||||
| otherwise ->
|
||||
writeTVar termState ts' {inputPrompt = "> ", previousInput}
|
||||
where
|
||||
previousInput = sendNameStr sendName <> " " <> s
|
||||
previousInput = chatNameStr chatName <> " " <> s
|
||||
_
|
||||
| live -> when (isSend s) $ do
|
||||
writeTVar termState ts' {previousInput = s}
|
||||
|
||||
@@ -55,7 +55,7 @@ data AutoCompleteState = ACState
|
||||
}
|
||||
|
||||
data LiveMessage = LiveMessage
|
||||
{ sendName :: SendName,
|
||||
{ chatName :: ChatName,
|
||||
chatItemId :: ChatItemId,
|
||||
livePrompt :: Bool,
|
||||
sentMsg :: String,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
module Simplex.Chat.Types where
|
||||
|
||||
import Crypto.Number.Serialize (os2ip)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.=))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Encoding as JE
|
||||
import qualified Data.Aeson.Types as JT
|
||||
@@ -233,7 +233,7 @@ data UserContactRequest = UserContactRequest
|
||||
agentInvitationId :: AgentInvId,
|
||||
userContactLinkId :: Int64,
|
||||
agentContactConnId :: AgentConnId, -- connection id of user contact
|
||||
cReqChatVRange :: VersionRange,
|
||||
cReqChatVRange :: JVersionRange,
|
||||
localDisplayName :: ContactName,
|
||||
profileId :: Int64,
|
||||
profile :: Profile,
|
||||
@@ -330,12 +330,12 @@ data GroupSummary = GroupSummary
|
||||
|
||||
instance ToJSON GroupSummary where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data ContactOrGroup = CGContact Contact | CGGroup GroupInfo
|
||||
data ContactOrGroup = CGContact Contact | CGGroup Group
|
||||
|
||||
contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId)
|
||||
contactAndGroupIds = \case
|
||||
CGContact Contact {contactId} -> (Just contactId, Nothing)
|
||||
CGGroup GroupInfo {groupId} -> (Nothing, Just groupId)
|
||||
CGGroup (Group GroupInfo {groupId} _) -> (Nothing, Just groupId)
|
||||
|
||||
-- TODO when more settings are added we should create another type to allow partial setting updates (with all Maybe properties)
|
||||
data ChatSettings = ChatSettings
|
||||
@@ -564,7 +564,7 @@ memberInfo :: GroupMember -> MemberInfo
|
||||
memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} =
|
||||
MemberInfo memberId memberRole memberChatVRange (fromLocalProfile memberProfile)
|
||||
where
|
||||
memberChatVRange = ChatVersionRange . peerChatVRange <$> activeConn
|
||||
memberChatVRange = ChatVersionRange . fromJVersionRange . peerChatVRange <$> activeConn
|
||||
|
||||
data ReceivedGroupInvitation = ReceivedGroupInvitation
|
||||
{ fromMember :: GroupMember,
|
||||
@@ -1167,7 +1167,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact
|
||||
data Connection = Connection
|
||||
{ connId :: Int64,
|
||||
agentConnId :: AgentConnId,
|
||||
peerChatVRange :: VersionRange,
|
||||
peerChatVRange :: JVersionRange,
|
||||
connLevel :: Int,
|
||||
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
|
||||
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
|
||||
@@ -1490,3 +1490,9 @@ instance FromJSON ChatVersionRange where
|
||||
instance ToJSON ChatVersionRange where
|
||||
toJSON (ChatVersionRange vr) = strToJSON vr
|
||||
toEncoding (ChatVersionRange vr) = strToJEncoding vr
|
||||
|
||||
newtype JVersionRange = JVersionRange {fromJVersionRange :: VersionRange} deriving (Eq, Show)
|
||||
|
||||
instance ToJSON JVersionRange where
|
||||
toJSON (JVersionRange (VersionRange minV maxV)) = J.object ["minVersion" .= minV, "maxVersion" .= maxV]
|
||||
toEncoding (JVersionRange (VersionRange minV maxV)) = J.pairs $ "minVersion" .= minV <> "maxVersion" .= maxV
|
||||
|
||||
@@ -322,35 +322,14 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
|
||||
testViewChat :: AChat -> [StyledString]
|
||||
testViewChat (AChat _ Chat {chatInfo, chatItems}) = [sShow $ map toChatView chatItems]
|
||||
where
|
||||
toChatView :: CChatItem c -> ((Int, String, Text), Maybe (Int, String, Text), Maybe String)
|
||||
toChatView ci@(CChatItem dir ChatItem {chatDir, quotedItem, file}) =
|
||||
(item, qItem, fPath)
|
||||
toChatView :: CChatItem c -> ((Int, Text), Maybe (Int, Text), Maybe String)
|
||||
toChatView ci@(CChatItem dir ChatItem {quotedItem, file}) =
|
||||
((msgDirectionInt $ toMsgDirection dir, testViewItem ci (chatInfoMembership chatInfo)), qItem, fPath)
|
||||
where
|
||||
item =
|
||||
( msgDirectionInt $ toMsgDirection dir,
|
||||
directMemberName,
|
||||
testViewItem ci (chatInfoMembership chatInfo)
|
||||
)
|
||||
directMemberName = case chatDir of
|
||||
CIGroupSnd (Just GroupMember {localDisplayName = n}) -> T.unpack n
|
||||
CIGroupRcv GroupMember {localDisplayName = n} MSDirect -> T.unpack n
|
||||
_ -> ""
|
||||
qItem = case quotedItem of
|
||||
Nothing -> Nothing
|
||||
Just CIQuote {chatDir = quoteDir, content} ->
|
||||
Just
|
||||
( msgDirectionInt $ quoteMsgDirection quoteDir,
|
||||
qMsgScope,
|
||||
msgContentText content
|
||||
)
|
||||
where
|
||||
qMsgScope = case quoteDir of
|
||||
CIQGroupSnd ms -> msgScopeText ms
|
||||
CIQGroupRcv _ ms -> msgScopeText ms
|
||||
_ -> ""
|
||||
msgScopeText ms = case ms of
|
||||
MSGroup -> "group"
|
||||
MSDirect -> "direct"
|
||||
Just (msgDirectionInt $ quoteMsgDirection quoteDir, msgContentText content)
|
||||
fPath = case file of
|
||||
Just CIFile {fileSource = Just (CryptoFile fp _)} -> Just fp
|
||||
_ -> Nothing
|
||||
@@ -401,7 +380,7 @@ viewUsersList = mapMaybe userInfo . sortOn ldn
|
||||
muted :: ChatInfo c -> CIDirection c d -> Bool
|
||||
muted chat chatDir = case (chat, chatDir) of
|
||||
(DirectChat Contact {chatSettings = DisableNtfs}, CIDirectRcv) -> True
|
||||
(GroupChat GroupInfo {chatSettings = DisableNtfs}, CIGroupRcv _ _) -> True
|
||||
(GroupChat GroupInfo {chatSettings = DisableNtfs}, CIGroupRcv _) -> True
|
||||
_ -> False
|
||||
|
||||
viewGroupSubscribed :: GroupInfo -> [StyledString]
|
||||
@@ -424,9 +403,8 @@ viewChats ts tz = concatMap chatPreview . reverse
|
||||
where
|
||||
chatName = case chat of
|
||||
DirectChat ct -> [" " <> ttyToContact' ct]
|
||||
GroupChat g -> [" " <> ttyToGroup' g]
|
||||
GroupChat g -> [" " <> ttyToGroup g]
|
||||
_ -> []
|
||||
ttyToGroup' g@GroupInfo {localDisplayName = n} = membershipIncognito g <> ttyTo ("#" <> n <> " ")
|
||||
|
||||
viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString]
|
||||
viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file} doShow ts tz =
|
||||
@@ -448,20 +426,20 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta, content, quotedItem, file}
|
||||
where
|
||||
quote = maybe [] (directQuote chatDir) quotedItem
|
||||
GroupChat g -> case chatDir of
|
||||
CIGroupSnd directMember -> case content of
|
||||
CIGroupSnd -> case content of
|
||||
CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to quote mc
|
||||
CISndGroupInvitation {} -> showSndItemProhibited to
|
||||
_ -> showSndItem to
|
||||
where
|
||||
to = ttyToGroup g directMember
|
||||
CIGroupRcv m msgScope -> case content of
|
||||
to = ttyToGroup g
|
||||
CIGroupRcv m -> case content of
|
||||
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
|
||||
CIRcvGroupInvitation {} -> showRcvItemProhibited from
|
||||
CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m msgScope) quote meta [plainContent content] False
|
||||
CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m) quote meta [plainContent content] False
|
||||
_ -> showRcvItem from
|
||||
where
|
||||
from = ttyFromGroup g m msgScope
|
||||
from = ttyFromGroup g m
|
||||
where
|
||||
quote = maybe [] (groupQuote g) quotedItem
|
||||
_ -> []
|
||||
@@ -553,18 +531,18 @@ viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemEdited, itemLive}
|
||||
where
|
||||
quote = maybe [] (directQuote chatDir) quotedItem
|
||||
GroupChat g -> case chatDir of
|
||||
CIGroupRcv m msgScope -> case content of
|
||||
CIGroupRcv m -> case content of
|
||||
CIRcvMsgContent mc
|
||||
| itemLive == Just True && not liveItems -> []
|
||||
| otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta
|
||||
_ -> []
|
||||
where
|
||||
from = if itemEdited then ttyFromGroupEdited g m msgScope else ttyFromGroup g m msgScope
|
||||
CIGroupSnd directMember -> case content of
|
||||
from = if itemEdited then ttyFromGroupEdited g m else ttyFromGroup g m
|
||||
CIGroupSnd -> case content of
|
||||
CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta
|
||||
_ -> []
|
||||
where
|
||||
to = if itemEdited then ttyToGroupEdited g directMember else ttyToGroup g directMember
|
||||
to = if itemEdited then ttyToGroupEdited g else ttyToGroup g
|
||||
where
|
||||
quote = maybe [] (groupQuote g) quotedItem
|
||||
_ -> []
|
||||
@@ -589,8 +567,7 @@ viewItemDelete chat ci@ChatItem {chatDir, meta, content = deletedContent} toItem
|
||||
GroupChat g -> case ciMsgContent deletedContent of
|
||||
Just mc ->
|
||||
let m = chatItemMember g ci
|
||||
msgScope = directMemberToMsgScope $ ciDirDirectMember chatDir
|
||||
in viewReceivedMessage (ttyFromGroupDeleted g m msgScope deletedText_) [] mc ts tz meta
|
||||
in viewReceivedMessage (ttyFromGroupDeleted g m deletedText_) [] mc ts tz meta
|
||||
_ -> prohibited
|
||||
_ -> prohibited
|
||||
where
|
||||
@@ -609,14 +586,14 @@ viewItemReaction showReactions chat CIReaction {chatDir, chatItem = CChatItem md
|
||||
where
|
||||
from = ttyFromContact c
|
||||
reactionMsg mc = quoteText mc $ if toMsgDirection md == MDSnd then ">>" else ">"
|
||||
(GroupChat g, CIGroupRcv m messageScope) -> case ciMsgContent content of
|
||||
(GroupChat g, CIGroupRcv m) -> case ciMsgContent content of
|
||||
Just mc -> view from $ reactionMsg mc
|
||||
_ -> []
|
||||
where
|
||||
from = ttyFromGroup g m messageScope
|
||||
from = ttyFromGroup g m
|
||||
reactionMsg mc = quoteText mc . ttyQuotedMember . Just $ sentByMember' g itemDir
|
||||
(_, CIDirectSnd) -> [sentText]
|
||||
(_, CIGroupSnd _) -> [sentText]
|
||||
(_, CIGroupSnd) -> [sentText]
|
||||
where
|
||||
view from msg
|
||||
| showReactions = viewReceivedReaction from msg reactionText ts tz sentAt
|
||||
@@ -644,13 +621,13 @@ groupQuote g CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc . ttyQu
|
||||
|
||||
sentByMember :: GroupInfo -> CIQDirection 'CTGroup -> Maybe GroupMember
|
||||
sentByMember GroupInfo {membership} = \case
|
||||
CIQGroupSnd _ -> Just membership
|
||||
CIQGroupRcv m _ -> m
|
||||
CIQGroupSnd -> Just membership
|
||||
CIQGroupRcv m -> m
|
||||
|
||||
sentByMember' :: GroupInfo -> CIDirection 'CTGroup d -> GroupMember
|
||||
sentByMember' GroupInfo {membership} = \case
|
||||
CIGroupSnd _ -> membership
|
||||
CIGroupRcv m _ -> m
|
||||
CIGroupSnd -> membership
|
||||
CIGroupRcv m -> m
|
||||
|
||||
quoteText :: MsgContent -> StyledString -> [StyledString]
|
||||
quoteText qmc sentBy = prependFirst (sentBy <> " ") $ msgPreview qmc
|
||||
@@ -1008,8 +985,8 @@ viewConnectionVerified :: Maybe SecurityCode -> StyledString
|
||||
viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time?
|
||||
viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code"
|
||||
|
||||
viewPeerChatVRange :: VersionRange -> StyledString
|
||||
viewPeerChatVRange (VersionRange minVer maxVer) = "peer chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")"
|
||||
viewPeerChatVRange :: JVersionRange -> StyledString
|
||||
viewPeerChatVRange (JVersionRange (VersionRange minVer maxVer)) = "peer chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")"
|
||||
|
||||
viewConnectionStats :: ConnectionStats -> [StyledString]
|
||||
viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} =
|
||||
@@ -1342,9 +1319,8 @@ sendingFile_ status ft@SndFileTransfer {recipientDisplayName = c} =
|
||||
uploadingFile :: StyledString -> AChatItem -> [StyledString]
|
||||
uploadingFile status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd}) =
|
||||
[status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyContact c]
|
||||
uploadingFile status (AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd directMember}) =
|
||||
let forMember = maybe "" (\GroupMember {localDisplayName = m} -> styled (colored Blue) $ " @" <> m <> " (direct)") directMember
|
||||
in [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g <> forMember]
|
||||
uploadingFile status (AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd}) =
|
||||
[status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g]
|
||||
uploadingFile status _ = [status <> " uploading file"] -- shouldn't happen
|
||||
|
||||
sndFile :: SndFileTransfer -> StyledString
|
||||
@@ -1376,7 +1352,7 @@ savingFile' :: Bool -> AChatItem -> [StyledString]
|
||||
savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath cfArgs_)}, chatDir}) =
|
||||
let from = case (chat, chatDir) of
|
||||
(DirectChat Contact {localDisplayName = c}, CIDirectRcv) -> " from " <> ttyContact c
|
||||
(_, CIGroupRcv GroupMember {localDisplayName = m} _) -> " from " <> ttyContact m
|
||||
(_, CIGroupRcv GroupMember {localDisplayName = m}) -> " from " <> ttyContact m
|
||||
_ -> ""
|
||||
in ["saving file " <> sShow fileId <> from <> " to " <> plain filePath] <> cfArgsStr
|
||||
where
|
||||
@@ -1390,7 +1366,7 @@ savingFile' _ _ = ["saving file"] -- shouldn't happen
|
||||
receivingFile_' :: StyledString -> AChatItem -> [StyledString]
|
||||
receivingFile_' status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) =
|
||||
[status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact c]
|
||||
receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv GroupMember {localDisplayName = m} _}) =
|
||||
receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv GroupMember {localDisplayName = m}}) =
|
||||
[status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact m]
|
||||
receivingFile_' status _ = [status <> " receiving file"] -- shouldn't happen
|
||||
|
||||
@@ -1606,7 +1582,7 @@ viewChatError logLevel = \case
|
||||
CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError]
|
||||
CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"]
|
||||
CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."]
|
||||
CEInvalidQuote -> ["invalid message reply"]
|
||||
CEInvalidQuote -> ["cannot reply to this message"]
|
||||
CEInvalidChatItemUpdate -> ["cannot update this item"]
|
||||
CEInvalidChatItemDelete -> ["cannot delete this item"]
|
||||
CEHasCurrentCall -> ["call already in progress"]
|
||||
@@ -1621,7 +1597,6 @@ viewChatError logLevel = \case
|
||||
CEAgentCommandError e -> ["agent command error: " <> plain e]
|
||||
CEInvalidFileDescription e -> ["invalid file description: " <> plain e]
|
||||
CEConnectionIncognitoChangeProhibited -> ["incognito mode change prohibited"]
|
||||
CEPeerChatVRangeIncompatible -> ["peer chat protocol version range incompatible"]
|
||||
CEInternalError e -> ["internal chat error: " <> plain e]
|
||||
CEException e -> ["exception: " <> plain e]
|
||||
-- e -> ["chat error: " <> sShow e]
|
||||
@@ -1762,24 +1737,19 @@ ttyFullGroup :: GroupInfo -> StyledString
|
||||
ttyFullGroup GroupInfo {localDisplayName = g, groupProfile = GroupProfile {fullName}} =
|
||||
ttyGroup g <> optFullName g fullName
|
||||
|
||||
ttyFromGroup :: GroupInfo -> GroupMember -> MessageScope -> StyledString
|
||||
ttyFromGroup g m ms = membershipIncognito g <> ttyFrom (fromGroup_ g m ms)
|
||||
ttyFromGroup :: GroupInfo -> GroupMember -> StyledString
|
||||
ttyFromGroup g m = membershipIncognito g <> ttyFrom (fromGroup_ g m)
|
||||
|
||||
ttyFromGroupEdited :: GroupInfo -> GroupMember -> MessageScope -> StyledString
|
||||
ttyFromGroupEdited g m ms = membershipIncognito g <> ttyFrom (fromGroup_ g m ms <> "[edited] ")
|
||||
ttyFromGroupEdited :: GroupInfo -> GroupMember -> StyledString
|
||||
ttyFromGroupEdited g m = membershipIncognito g <> ttyFrom (fromGroup_ g m <> "[edited] ")
|
||||
|
||||
ttyFromGroupDeleted :: GroupInfo -> GroupMember -> MessageScope -> Maybe Text -> StyledString
|
||||
ttyFromGroupDeleted g m ms deletedText_ =
|
||||
membershipIncognito g <> ttyFrom (fromGroup_ g m ms <> maybe "" (\t -> "[" <> t <> "] ") deletedText_)
|
||||
ttyFromGroupDeleted :: GroupInfo -> GroupMember -> Maybe Text -> StyledString
|
||||
ttyFromGroupDeleted g m deletedText_ =
|
||||
membershipIncognito g <> ttyFrom (fromGroup_ g m <> maybe "" (\t -> "[" <> t <> "] ") deletedText_)
|
||||
|
||||
fromGroup_ :: GroupInfo -> GroupMember -> MessageScope -> Text
|
||||
fromGroup_ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} ms =
|
||||
"#" <> g <> " " <> m <> fromGroupScope ms <> "> "
|
||||
|
||||
fromGroupScope :: MessageScope -> Text
|
||||
fromGroupScope = \case
|
||||
MSGroup -> ""
|
||||
MSDirect -> " (direct)"
|
||||
fromGroup_ :: GroupInfo -> GroupMember -> Text
|
||||
fromGroup_ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} =
|
||||
"#" <> g <> " " <> m <> "> "
|
||||
|
||||
ttyFrom :: Text -> StyledString
|
||||
ttyFrom = styled $ colored Yellow
|
||||
@@ -1787,18 +1757,13 @@ ttyFrom = styled $ colored Yellow
|
||||
ttyTo :: Text -> StyledString
|
||||
ttyTo = styled $ colored Cyan
|
||||
|
||||
ttyToGroup :: GroupInfo -> Maybe GroupMember -> StyledString
|
||||
ttyToGroup g@GroupInfo {localDisplayName = n} dirMem =
|
||||
membershipIncognito g <> ttyTo ("#" <> n <> toDirectMember dirMem <> " ")
|
||||
ttyToGroup :: GroupInfo -> StyledString
|
||||
ttyToGroup g@GroupInfo {localDisplayName = n} =
|
||||
membershipIncognito g <> ttyTo ("#" <> n <> " ")
|
||||
|
||||
ttyToGroupEdited :: GroupInfo -> Maybe GroupMember -> StyledString
|
||||
ttyToGroupEdited g@GroupInfo {localDisplayName = n} dirMem =
|
||||
membershipIncognito g <> ttyTo ("#" <> n <> toDirectMember dirMem <> " [edited] ")
|
||||
|
||||
toDirectMember :: Maybe GroupMember -> Text
|
||||
toDirectMember = \case
|
||||
Nothing -> ""
|
||||
Just GroupMember {localDisplayName = m} -> " @" <> m <> " (direct)"
|
||||
ttyToGroupEdited :: GroupInfo -> StyledString
|
||||
ttyToGroupEdited g@GroupInfo {localDisplayName = n} =
|
||||
membershipIncognito g <> ttyTo ("#" <> n <> " [edited] ")
|
||||
|
||||
ttyFilePath :: FilePath -> StyledString
|
||||
ttyFilePath = plain
|
||||
|
||||
@@ -259,7 +259,7 @@ getTermLine cc =
|
||||
Just s -> do
|
||||
-- remove condition to always echo virtual terminal
|
||||
when (printOutput cc) $ do
|
||||
-- when True $ do
|
||||
-- when True $ do
|
||||
name <- userName cc
|
||||
putStrLn $ name <> ": " <> s
|
||||
pure s
|
||||
|
||||
@@ -8,9 +8,8 @@ import ChatTests.Utils
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Monad (when)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Controller (ChatConfig (..), XFTPFileConfig (..))
|
||||
import Simplex.Chat.Controller (ChatConfig (..))
|
||||
import Simplex.Chat.Protocol (supportedChatVRange)
|
||||
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
|
||||
import Simplex.Chat.Types (GroupMemberRole (..))
|
||||
@@ -82,21 +81,6 @@ chatGroupTests = do
|
||||
testNoDirect4 _1 _0 _1 False False False -- False False True
|
||||
testNoDirect4 _1 _1 _0 False False False
|
||||
testNoDirect4 _1 _1 _1 False False False
|
||||
describe "group direct messages" $ do
|
||||
it "should send group direct messages" testGroupDirectMessages
|
||||
it "should create group direct messages chat items" testGroupDirectMessagesItems
|
||||
it "should send group direct quotes" testGroupDirectQuotes
|
||||
it "should create group direct quotes chat items" testGroupDirectQuotesItems
|
||||
it "should send group direct XFTP files" testGroupDirectFilesXFTP
|
||||
it "should send group direct SMP files" testGroupDirectFilesSMP
|
||||
it "should cancel sent group direct XFTP file" testGroupDirectCancelFileXFTP
|
||||
it "should send group direct quotes with files" testGroupDirectQuotesFiles
|
||||
it "should update group direct message" testGroupDirectUpdate
|
||||
it "should delete group direct message" testGroupDirectDelete
|
||||
it "should send group direct live message" testGroupDirectLiveMessage
|
||||
it "should send group direct message reactions" testGroupDirectReactions
|
||||
it "should prohibit group direct messages based on preference" testGroupDirectProhibitPreference
|
||||
it "should prohibit group direct messages if peer version doesn't support" testGroupDirectProhibitNotSupported
|
||||
where
|
||||
_0 = supportedChatVRange -- don't create direct connections
|
||||
_1 = groupCreateDirectVRange
|
||||
@@ -820,7 +804,7 @@ testGroupMessageQuotedReply =
|
||||
(bob <# "#team alice> hello! how are you?")
|
||||
(cath <# "#team alice> hello! how are you?")
|
||||
threadDelay 1000000
|
||||
bob `send` "> #team >@alice (hello) hello, all good, you?"
|
||||
bob `send` "> #team @alice (hello) hello, all good, you?"
|
||||
bob <# "#team > alice hello! how are you?"
|
||||
bob <## " hello, all good, you?"
|
||||
concurrently_
|
||||
@@ -835,7 +819,7 @@ testGroupMessageQuotedReply =
|
||||
bob #$> ("/_get chat #1 count=2", chat', [((0, "hello! how are you?"), Nothing), ((1, "hello, all good, you?"), Just (0, "hello! how are you?"))])
|
||||
alice #$> ("/_get chat #1 count=2", chat', [((1, "hello! how are you?"), Nothing), ((0, "hello, all good, you?"), Just (1, "hello! how are you?"))])
|
||||
cath #$> ("/_get chat #1 count=2", chat', [((0, "hello! how are you?"), Nothing), ((0, "hello, all good, you?"), Just (0, "hello! how are you?"))])
|
||||
bob `send` "> #team >@bob (hello, all good) will tell more"
|
||||
bob `send` "> #team bob (hello, all good) will tell more"
|
||||
bob <# "#team > bob hello, all good, you?"
|
||||
bob <## " will tell more"
|
||||
concurrently_
|
||||
@@ -851,7 +835,7 @@ testGroupMessageQuotedReply =
|
||||
alice #$> ("/_get chat #1 count=1", chat', [((0, "will tell more"), Just (0, "hello, all good, you?"))])
|
||||
cath #$> ("/_get chat #1 count=1", chat', [((0, "will tell more"), Just (0, "hello, all good, you?"))])
|
||||
threadDelay 1000000
|
||||
cath `send` "> #team >@bob (hello) hi there!"
|
||||
cath `send` "> #team bob (hello) hi there!"
|
||||
cath <# "#team > bob hello, all good, you?"
|
||||
cath <## " hi there!"
|
||||
concurrently_
|
||||
@@ -907,7 +891,7 @@ testGroupMessageUpdate =
|
||||
|
||||
threadDelay 1000000
|
||||
-- alice, bob: msg id 6, cath: msg id 5
|
||||
bob `send` "> #team >@alice (hey) hi alice"
|
||||
bob `send` "> #team @alice (hey) hi alice"
|
||||
bob <# "#team > alice hey 👋"
|
||||
bob <## " hi alice"
|
||||
concurrently_
|
||||
@@ -934,7 +918,7 @@ testGroupMessageUpdate =
|
||||
alice #$> ("/_update item #1 " <> msgItemId2 <> " text updating bob's message", id, "cannot update this item")
|
||||
|
||||
threadDelay 1000000
|
||||
cath `send` "> #team >@alice (greetings) greetings!"
|
||||
cath `send` "> #team @alice (greetings) greetings!"
|
||||
cath <# "#team > alice greetings 🤝"
|
||||
cath <## " greetings!"
|
||||
concurrently_
|
||||
@@ -1010,6 +994,7 @@ testGroupMessageEditHistory =
|
||||
|
||||
alice ##> ("/_update item #1 " <> aliceItemId <> " text hey there")
|
||||
alice <# "#team [edited] hey there"
|
||||
bob <# "#team alice> [edited] hey there"
|
||||
|
||||
alice ##> "/item info #team hey"
|
||||
alice <##. "sent at: "
|
||||
@@ -1019,7 +1004,10 @@ testGroupMessageEditHistory =
|
||||
alice .<## ": hey 👋"
|
||||
alice .<## ": hello!"
|
||||
bob ##> "/item info #team hey"
|
||||
bob <## "message not found by text: hey"
|
||||
bob <##. "sent at: "
|
||||
bob <##. "received at: "
|
||||
bob <## "message history:"
|
||||
bob .<## ": hey there"
|
||||
|
||||
testGroupMessageDelete :: HasCallStack => FilePath -> IO ()
|
||||
testGroupMessageDelete =
|
||||
@@ -1043,7 +1031,7 @@ testGroupMessageDelete =
|
||||
|
||||
threadDelay 1000000
|
||||
-- alice: msg id 5, bob: msg id 6, cath: msg id 5
|
||||
bob `send` "> #team >@alice (hello) hi alic"
|
||||
bob `send` "> #team @alice (hello) hi alic"
|
||||
bob <# "#team > alice hello!"
|
||||
bob <## " hi alic"
|
||||
concurrently_
|
||||
@@ -1072,10 +1060,14 @@ testGroupMessageDelete =
|
||||
bob ##> ("/_update item #1 " <> msgItemId3 <> " text hi alice")
|
||||
bob <# "#team [edited] > alice hello!"
|
||||
bob <## " hi alice"
|
||||
cath <# "#team bob> [edited] > alice hello!"
|
||||
cath <## " hi alice"
|
||||
concurrently_
|
||||
(alice <# "#team bob> [edited] hi alice")
|
||||
( do
|
||||
cath <# "#team bob> [edited] > alice hello!"
|
||||
cath <## " hi alice"
|
||||
)
|
||||
|
||||
alice #$> ("/_get chat #1 count=1", chat', [((0, "connected"), Nothing)])
|
||||
alice #$> ("/_get chat #1 count=1", chat', [((0, "hi alice"), Nothing)])
|
||||
bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!"))])
|
||||
cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!"))])
|
||||
|
||||
@@ -2694,534 +2686,3 @@ testNoGroupDirectConns4Members hostVRange mem2VRange mem3VRange mem4VRange noCon
|
||||
cc1 <## ("no contact " <> name2)
|
||||
cc2 ##> ("@" <> name1 <> " hi")
|
||||
cc2 <## ("no contact " <> name1)
|
||||
|
||||
testGroupDirectMessages :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectMessages =
|
||||
testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
connectUsers alice dan
|
||||
addMember "team" alice dan GRMember
|
||||
dan ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: dan joined the group",
|
||||
do
|
||||
dan <## "#team: you joined the group"
|
||||
dan
|
||||
<### [ "#team: member bob (Bob) is connected",
|
||||
"#team: member cath (Catherine) is connected"
|
||||
],
|
||||
aliceAddedDan bob,
|
||||
aliceAddedDan cath
|
||||
]
|
||||
|
||||
alice #> "#team hi"
|
||||
bob <# "#team alice> hi"
|
||||
cath <# "#team alice> hi"
|
||||
dan <# "#team alice> hi"
|
||||
|
||||
alice `send` "#team @bob hi bob"
|
||||
alice <# "#team @bob (direct) hi bob"
|
||||
bob <# "#team alice (direct)> hi bob"
|
||||
|
||||
bob `send` "#team @alice hi alice"
|
||||
bob <# "#team @alice (direct) hi alice"
|
||||
alice <# "#team bob (direct)> hi alice"
|
||||
|
||||
dan #> "#team hello"
|
||||
alice <# "#team dan> hello"
|
||||
bob <# "#team dan> hello"
|
||||
cath <# "#team dan> hello"
|
||||
|
||||
bob `send` "#team @cath hi cath"
|
||||
bob <# "#team @cath (direct) hi cath"
|
||||
cath <# "#team bob (direct)> hi cath"
|
||||
|
||||
cath `send` "#team @bob hello bob"
|
||||
cath <# "#team @bob (direct) hello bob"
|
||||
bob <# "#team cath (direct)> hello bob"
|
||||
where
|
||||
aliceAddedDan :: HasCallStack => TestCC -> IO ()
|
||||
aliceAddedDan cc = do
|
||||
cc <## "#team: alice added dan (Daniel) to the group (connecting...)"
|
||||
cc <## "#team: new member dan is connected"
|
||||
|
||||
testGroupDirectMessagesItems :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectMessagesItems =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
threadDelay 1000000
|
||||
|
||||
alice #> "#team hi"
|
||||
bob <# "#team alice> hi"
|
||||
cath <# "#team alice> hi"
|
||||
threadDelay 1000000
|
||||
|
||||
alice `send` "#team @bob hi bob"
|
||||
alice <# "#team @bob (direct) hi bob"
|
||||
bob <# "#team alice (direct)> hi bob"
|
||||
threadDelay 1000000
|
||||
|
||||
bob `send` "#team @alice hi alice"
|
||||
bob <# "#team @alice (direct) hi alice"
|
||||
alice <# "#team bob (direct)> hi alice"
|
||||
threadDelay 1000000
|
||||
|
||||
alice #$> ("/_get chat #1 count=4", mapChat, [(0, "", "connected"), (1, "", "hi"), (1, "bob", "hi bob"), (0, "bob", "hi alice")])
|
||||
bob #$> ("/_get chat #1 count=4", mapChat, [(0, "", "connected"), (0, "", "hi"), (0, "alice", "hi bob"), (1, "alice", "hi alice")])
|
||||
cath #$> ("/_get chat #1 count=2", mapChat, [(0, "", "connected"), (0, "", "hi")])
|
||||
where
|
||||
mapChat = map (\(a, _, _) -> a) . chat'''
|
||||
|
||||
testGroupDirectQuotes :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectQuotes =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
alice #> "#team 1-g-a"
|
||||
bob <# "#team alice> 1-g-a"
|
||||
cath <# "#team alice> 1-g-a"
|
||||
|
||||
bob #> "#team 2-g-b"
|
||||
alice <# "#team bob> 2-g-b"
|
||||
cath <# "#team bob> 2-g-b"
|
||||
|
||||
cath #> "#team 3-g-c"
|
||||
alice <# "#team cath> 3-g-c"
|
||||
bob <# "#team cath> 3-g-c"
|
||||
|
||||
alice `send` "#team @bob 4-p-ab"
|
||||
alice <# "#team @bob (direct) 4-p-ab"
|
||||
bob <# "#team alice (direct)> 4-p-ab"
|
||||
|
||||
bob `send` "#team @alice 5-p-ba"
|
||||
bob <# "#team @alice (direct) 5-p-ba"
|
||||
alice <# "#team bob (direct)> 5-p-ba"
|
||||
|
||||
alice `send` "#team @cath 6-p-ac"
|
||||
alice <# "#team @cath (direct) 6-p-ac"
|
||||
cath <# "#team alice (direct)> 6-p-ac"
|
||||
|
||||
cath `send` "#team @alice 7-p-ca"
|
||||
cath <# "#team @alice (direct) 7-p-ca"
|
||||
alice <# "#team cath (direct)> 7-p-ca"
|
||||
|
||||
-- quotes
|
||||
|
||||
alice `send` "> #team @bob (1-g-a) 8-pq-ab"
|
||||
alice <# "#team @bob (direct) > alice 1-g-a"
|
||||
alice <## " 8-pq-ab"
|
||||
bob <# "#team alice (direct)> > alice 1-g-a"
|
||||
bob <## " 8-pq-ab"
|
||||
|
||||
alice `send` "> #team @bob (2-g-b) 9-pq-ab"
|
||||
alice <# "#team @bob (direct) > bob 2-g-b"
|
||||
alice <## " 9-pq-ab"
|
||||
bob <# "#team alice (direct)> > bob 2-g-b"
|
||||
bob <## " 9-pq-ab"
|
||||
|
||||
alice `send` "> #team >@cath @bob (3-g-c) 10-pq-ab"
|
||||
alice <# "#team @bob (direct) > cath 3-g-c"
|
||||
alice <## " 10-pq-ab"
|
||||
bob <# "#team alice (direct)> > cath 3-g-c"
|
||||
bob <## " 10-pq-ab"
|
||||
|
||||
alice `send` "> #team @bob (4-p-ab) 11-pq-ab"
|
||||
alice <# "#team @bob (direct) > alice 4-p-ab"
|
||||
alice <## " 11-pq-ab"
|
||||
bob <# "#team alice (direct)> > alice 4-p-ab"
|
||||
bob <## " 11-pq-ab"
|
||||
|
||||
alice `send` "> #team >@bob @bob (5-p-ba) 12-pq-ab"
|
||||
alice <# "#team @bob (direct) > bob 5-p-ba"
|
||||
alice <## " 12-pq-ab"
|
||||
bob <# "#team alice (direct)> > bob 5-p-ba"
|
||||
bob <## " 12-pq-ab"
|
||||
|
||||
alice `send` "> #team @bob (6-p-ac) 13-pq-ab"
|
||||
alice <## "> #team @bob (6-p-ac) 13-pq-ab"
|
||||
alice <## "invalid message reply"
|
||||
|
||||
alice `send` "> #team @bob (7-p-ca) 14-pq-ab"
|
||||
alice <## "> #team @bob (7-p-ca) 14-pq-ab"
|
||||
alice <## "invalid message reply"
|
||||
|
||||
alice `send` "> #team (4-p-ab) 15-gq-a"
|
||||
alice <## "> #team (4-p-ab) 15-gq-a"
|
||||
alice <## "invalid message reply"
|
||||
|
||||
alice `send` "> #team (5-p-ba) 16-gq-a"
|
||||
alice <## "> #team (5-p-ba) 16-gq-a"
|
||||
alice <## "invalid message reply"
|
||||
|
||||
testGroupDirectQuotesItems :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectQuotesItems =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
alice #> "#team 1-g-a"
|
||||
bob <# "#team alice> 1-g-a"
|
||||
cath <# "#team alice> 1-g-a"
|
||||
|
||||
alice `send` "#team @bob 2-p-ab"
|
||||
alice <# "#team @bob (direct) 2-p-ab"
|
||||
bob <# "#team alice (direct)> 2-p-ab"
|
||||
|
||||
bob `send` "#team @alice 3-p-ba"
|
||||
bob <# "#team @alice (direct) 3-p-ba"
|
||||
alice <# "#team bob (direct)> 3-p-ba"
|
||||
threadDelay 1000000
|
||||
|
||||
-- quotes
|
||||
|
||||
alice `send` "> #team @bob (1-g-a) 4-pq-ab"
|
||||
alice <# "#team @bob (direct) > alice 1-g-a"
|
||||
alice <## " 4-pq-ab"
|
||||
bob <# "#team alice (direct)> > alice 1-g-a"
|
||||
bob <## " 4-pq-ab"
|
||||
threadDelay 1000000
|
||||
|
||||
alice `send` "> #team @bob (2-p-ab) 5-pq-ab"
|
||||
alice <# "#team @bob (direct) > alice 2-p-ab"
|
||||
alice <## " 5-pq-ab"
|
||||
bob <# "#team alice (direct)> > alice 2-p-ab"
|
||||
bob <## " 5-pq-ab"
|
||||
threadDelay 1000000
|
||||
|
||||
alice `send` "> #team >@bob @bob (3-p-ba) 6-pq-ab"
|
||||
alice <# "#team @bob (direct) > bob 3-p-ba"
|
||||
alice <## " 6-pq-ab"
|
||||
bob <# "#team alice (direct)> > bob 3-p-ba"
|
||||
bob <## " 6-pq-ab"
|
||||
|
||||
alice
|
||||
#$> ( "/_get chat #1 count=3",
|
||||
mapChat,
|
||||
[ ((1, "bob", "4-pq-ab"), Just (1, "group", "1-g-a")),
|
||||
((1, "bob", "5-pq-ab"), Just (1, "direct", "2-p-ab")),
|
||||
((1, "bob", "6-pq-ab"), Just (0, "direct", "3-p-ba"))
|
||||
]
|
||||
)
|
||||
bob
|
||||
#$> ( "/_get chat #1 count=3",
|
||||
mapChat,
|
||||
[ ((0, "alice", "4-pq-ab"), Just (0, "group", "1-g-a")),
|
||||
((0, "alice", "5-pq-ab"), Just (0, "direct", "2-p-ab")),
|
||||
((0, "alice", "6-pq-ab"), Just (1, "direct", "3-p-ba"))
|
||||
]
|
||||
)
|
||||
where
|
||||
mapChat = map (\(a, b, _) -> (a, b)) . chat'''
|
||||
|
||||
testGroupDirectFilesXFTP :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectFilesXFTP =
|
||||
testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
withXFTPServer $ do
|
||||
createGroup3 "team" alice bob cath
|
||||
threadDelay 1000000
|
||||
|
||||
alice `send` "/f #team @bob ./tests/fixtures/test.pdf"
|
||||
alice <# "/f #team @bob (direct) ./tests/fixtures/test.pdf"
|
||||
alice <## "use /fc 1 to cancel sending"
|
||||
bob <# "#team alice (direct)> sends file test.pdf (266.0 KiB / 272376 bytes)"
|
||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
alice <## "completed uploading file 1 (test.pdf) for #team @bob (direct)"
|
||||
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob
|
||||
<### [ "saving file 1 from alice to ./tests/tmp/test.pdf",
|
||||
"started receiving file 1 (test.pdf) from alice"
|
||||
]
|
||||
bob <## "completed receiving file 1 (test.pdf) from alice"
|
||||
|
||||
src <- B.readFile "./tests/fixtures/test.pdf"
|
||||
dest <- B.readFile "./tests/tmp/test.pdf"
|
||||
dest `shouldBe` src
|
||||
|
||||
cath <// 50000
|
||||
cath ##> "/fr 1 ./tests/tmp"
|
||||
cath <##. "chat db error: SEUserNotFoundByFileId"
|
||||
|
||||
alice `send` "/f #team @cath ./tests/fixtures/test.jpg"
|
||||
alice <# "/f #team @cath (direct) ./tests/fixtures/test.jpg"
|
||||
alice <## "use /fc 2 to cancel sending"
|
||||
cath <# "#team alice (direct)> sends file test.jpg (136.5 KiB / 139737 bytes)"
|
||||
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
alice <## "completed uploading file 2 (test.jpg) for #team @cath (direct)"
|
||||
|
||||
cath ##> "/fr 1 ./tests/tmp"
|
||||
cath
|
||||
<### [ "saving file 1 from alice to ./tests/tmp/test.jpg",
|
||||
"started receiving file 1 (test.jpg) from alice"
|
||||
]
|
||||
cath <## "completed receiving file 1 (test.jpg) from alice"
|
||||
|
||||
src2 <- B.readFile "./tests/fixtures/test.jpg"
|
||||
dest2 <- B.readFile "./tests/tmp/test.jpg"
|
||||
dest2 `shouldBe` src2
|
||||
|
||||
bob <// 50000
|
||||
|
||||
alice #$> ("/_get chat #1 count=2", mapChat, [((1, "bob", ""), Just "./tests/fixtures/test.pdf"), ((1, "cath", ""), Just "./tests/fixtures/test.jpg")])
|
||||
bob #$> ("/_get chat #1 count=2", mapChat, [((0, "", "connected"), Nothing), ((0, "alice", ""), Just "./tests/tmp/test.pdf")])
|
||||
cath #$> ("/_get chat #1 count=2", mapChat, [((0, "", "connected"), Nothing), ((0, "alice", ""), Just "./tests/tmp/test.jpg")])
|
||||
where
|
||||
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
|
||||
mapChat = map (\(a, _, c) -> (a, c)) . chat'''
|
||||
|
||||
testGroupDirectFilesSMP :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectFilesSMP =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
threadDelay 1000000
|
||||
|
||||
alice `send` "/f #team @bob ./tests/fixtures/test.pdf"
|
||||
alice <# "/f #team @bob (direct) ./tests/fixtures/test.pdf"
|
||||
alice <## "use /fc 1 to cancel sending"
|
||||
bob <# "#team alice (direct)> sends file test.pdf (266.0 KiB / 272376 bytes)"
|
||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
|
||||
concurrently_
|
||||
(alice <## "started sending file 1 (test.pdf) to bob")
|
||||
(bob <## "started receiving file 1 (test.pdf) from alice")
|
||||
concurrently_
|
||||
(alice <## "completed sending file 1 (test.pdf) to bob")
|
||||
(bob <## "completed receiving file 1 (test.pdf) from alice")
|
||||
|
||||
src <- B.readFile "./tests/fixtures/test.pdf"
|
||||
dest <- B.readFile "./tests/tmp/test.pdf"
|
||||
dest `shouldBe` src
|
||||
|
||||
cath <// 50000
|
||||
cath ##> "/fr 1 ./tests/tmp"
|
||||
cath <##. "chat db error: SEUserNotFoundByFileId"
|
||||
|
||||
alice `send` "/f #team @cath ./tests/fixtures/test.jpg"
|
||||
alice <# "/f #team @cath (direct) ./tests/fixtures/test.jpg"
|
||||
alice <## "use /fc 2 to cancel sending"
|
||||
cath <# "#team alice (direct)> sends file test.jpg (136.5 KiB / 139737 bytes)"
|
||||
cath <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
cath ##> "/fr 1 ./tests/tmp"
|
||||
cath <## "saving file 1 from alice to ./tests/tmp/test.jpg"
|
||||
concurrently_
|
||||
(alice <## "started sending file 2 (test.jpg) to cath")
|
||||
(cath <## "started receiving file 1 (test.jpg) from alice")
|
||||
concurrently_
|
||||
(alice <## "completed sending file 2 (test.jpg) to cath")
|
||||
(cath <## "completed receiving file 1 (test.jpg) from alice")
|
||||
|
||||
src2 <- B.readFile "./tests/fixtures/test.jpg"
|
||||
dest2 <- B.readFile "./tests/tmp/test.jpg"
|
||||
dest2 `shouldBe` src2
|
||||
|
||||
bob <// 50000
|
||||
|
||||
alice #$> ("/_get chat #1 count=2", mapChat, [((1, "bob", ""), Just "./tests/fixtures/test.pdf"), ((1, "cath", ""), Just "./tests/fixtures/test.jpg")])
|
||||
bob #$> ("/_get chat #1 count=2", mapChat, [((0, "", "connected"), Nothing), ((0, "alice", ""), Just "./tests/tmp/test.pdf")])
|
||||
cath #$> ("/_get chat #1 count=2", mapChat, [((0, "", "connected"), Nothing), ((0, "alice", ""), Just "./tests/tmp/test.jpg")])
|
||||
where
|
||||
mapChat = map (\(a, _, c) -> (a, c)) . chat'''
|
||||
|
||||
testGroupDirectCancelFileXFTP :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectCancelFileXFTP =
|
||||
testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
withXFTPServer $ do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
alice `send` "/f #team @bob ./tests/fixtures/test.pdf"
|
||||
alice <# "/f #team @bob (direct) ./tests/fixtures/test.pdf"
|
||||
alice <## "use /fc 1 to cancel sending"
|
||||
bob <# "#team alice (direct)> sends file test.pdf (266.0 KiB / 272376 bytes)"
|
||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
alice <## "completed uploading file 1 (test.pdf) for #team @bob (direct)"
|
||||
|
||||
cath <// 50000
|
||||
|
||||
alice ##> "/fc 1"
|
||||
alice <## "cancelled sending file 1 (test.pdf) to bob"
|
||||
bob <## "alice cancelled sending file 1 (test.pdf)"
|
||||
|
||||
cath <// 50000
|
||||
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## "file cancelled: test.pdf"
|
||||
|
||||
cath ##> "/fr 1 ./tests/tmp"
|
||||
cath <##. "chat db error: SEUserNotFoundByFileId"
|
||||
where
|
||||
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
|
||||
|
||||
testGroupDirectQuotesFiles :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectQuotesFiles =
|
||||
testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
withXFTPServer $ do
|
||||
createGroup3 "team" alice bob cath
|
||||
threadDelay 1000000
|
||||
|
||||
bob `send` "#team @alice hi alice"
|
||||
bob <# "#team @alice (direct) hi alice"
|
||||
alice <# "#team bob (direct)> hi alice"
|
||||
threadDelay 1000000
|
||||
|
||||
msgItemId1 <- lastItemId alice
|
||||
alice ##> ("/_send #1 @2 json {\"filePath\": \"./tests/fixtures/test.pdf\", \"quotedItemId\": " <> msgItemId1 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}")
|
||||
alice <# "#team @bob (direct) > bob hi alice"
|
||||
alice <## " hey bob"
|
||||
alice <# "/f #team @bob (direct) ./tests/fixtures/test.pdf"
|
||||
alice <## "use /fc 1 to cancel sending"
|
||||
bob <# "#team alice (direct)> > bob hi alice"
|
||||
bob <## " hey bob"
|
||||
bob <# "#team alice (direct)> sends file test.pdf (266.0 KiB / 272376 bytes)"
|
||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
alice <## "completed uploading file 1 (test.pdf) for #team @bob (direct)"
|
||||
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob
|
||||
<### [ "saving file 1 from alice to ./tests/tmp/test.pdf",
|
||||
"started receiving file 1 (test.pdf) from alice"
|
||||
]
|
||||
bob <## "completed receiving file 1 (test.pdf) from alice"
|
||||
|
||||
src <- B.readFile "./tests/fixtures/test.pdf"
|
||||
dest <- B.readFile "./tests/tmp/test.pdf"
|
||||
dest `shouldBe` src
|
||||
|
||||
cath <// 50000
|
||||
cath ##> "/fr 1 ./tests/tmp"
|
||||
cath <##. "chat db error: SEUserNotFoundByFileId"
|
||||
|
||||
alice
|
||||
#$> ( "/_get chat #1 count=2",
|
||||
chat''',
|
||||
[ ((0, "bob", "hi alice"), Nothing, Nothing),
|
||||
((1, "bob", "hey bob"), Just (0, "direct", "hi alice"), Just "./tests/fixtures/test.pdf")
|
||||
]
|
||||
)
|
||||
bob
|
||||
#$> ( "/_get chat #1 count=2",
|
||||
chat''',
|
||||
[ ((1, "alice", "hi alice"), Nothing, Nothing),
|
||||
((0, "alice", "hey bob"), Just (1, "direct", "hi alice"), Just "./tests/tmp/test.pdf")
|
||||
]
|
||||
)
|
||||
cath #$> ("/_get chat #1 count=1", chat''', [((0, "", "connected"), Nothing, Nothing)])
|
||||
where
|
||||
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
|
||||
|
||||
testGroupDirectUpdate :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectUpdate =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
alice `send` "#team @bob hi bob"
|
||||
alice <# "#team @bob (direct) hi bob"
|
||||
bob <# "#team alice (direct)> hi bob"
|
||||
|
||||
msgItemId1 <- lastItemId alice
|
||||
alice ##> ("/_update item #1 " <> msgItemId1 <> " text hey 👋")
|
||||
alice <# "#team @bob (direct) [edited] hey 👋"
|
||||
bob <# "#team alice (direct)> [edited] hey 👋"
|
||||
cath <// 50000
|
||||
|
||||
alice ##> "! #team (hey 👋) hello there"
|
||||
alice <# "#team @bob (direct) [edited] hello there"
|
||||
bob <# "#team alice (direct)> [edited] hello there"
|
||||
cath <// 50000
|
||||
|
||||
testGroupDirectDelete :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectDelete =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
alice `send` "#team @bob hi bob"
|
||||
alice <# "#team @bob (direct) hi bob"
|
||||
bob <# "#team alice (direct)> hi bob"
|
||||
|
||||
msgItemId1 <- lastItemId alice
|
||||
alice #$> ("/_delete item #1 " <> msgItemId1 <> " broadcast", id, "message marked deleted")
|
||||
bob <# "#team alice (direct)> [marked deleted] hi bob"
|
||||
cath <// 50000
|
||||
|
||||
testGroupDirectLiveMessage :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectLiveMessage =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
alice `send` "/live #team @bob hello"
|
||||
msgItemId1 <- lastItemId alice
|
||||
bob <#. "#team alice (direct)> [LIVE started]"
|
||||
alice ##> ("/_update item #1 " <> msgItemId1 <> " text hello there")
|
||||
alice <# "#team @bob (direct) [LIVE] hello there"
|
||||
bob <# "#team alice (direct)> [LIVE ended] hello there"
|
||||
cath <// 50000
|
||||
|
||||
testGroupDirectReactions :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectReactions =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
alice `send` "#team @bob hi bob"
|
||||
alice <# "#team @bob (direct) hi bob"
|
||||
bob <# "#team alice (direct)> hi bob"
|
||||
|
||||
bob ##> "+1 #team hi"
|
||||
bob <## "added 👍"
|
||||
alice <# "#team bob (direct)> > alice hi bob"
|
||||
alice <## " + 👍"
|
||||
cath <// 50000
|
||||
|
||||
alice ##> "+^ #team hi"
|
||||
alice <## "added 🚀"
|
||||
bob <# "#team alice (direct)> > alice hi bob"
|
||||
bob <## " + 🚀"
|
||||
cath <// 50000
|
||||
|
||||
testGroupDirectProhibitPreference :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectProhibitPreference =
|
||||
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||
createGroup3' "team" alice bob cath GRMember
|
||||
|
||||
alice ##> "/set direct #team off"
|
||||
alice <## "updated group preferences:"
|
||||
alice <## "Direct messages: off"
|
||||
directProhibited bob
|
||||
directProhibited cath
|
||||
|
||||
bob ##> "#team @cath hi cath"
|
||||
bob <## "bad chat command: direct messages not allowed"
|
||||
|
||||
cath ##> "#team @bob hi cath"
|
||||
cath <## "bad chat command: direct messages not allowed"
|
||||
|
||||
alice ##> "/mr team bob admin"
|
||||
alice <## "#team: you changed the role of bob from member to admin"
|
||||
concurrentlyN_
|
||||
[ bob <## "#team: alice changed your role from member to admin",
|
||||
cath <## "#team: alice changed the role of bob from member to admin"
|
||||
]
|
||||
|
||||
-- admin can send & can send to admin
|
||||
|
||||
bob `send` "#team @cath hi cath, as admin"
|
||||
bob <# "#team @cath (direct) hi cath, as admin"
|
||||
cath <# "#team bob (direct)> hi cath, as admin"
|
||||
|
||||
cath `send` "#team @bob hi bob, to admin"
|
||||
cath <# "#team @bob (direct) hi bob, to admin"
|
||||
bob <# "#team cath (direct)> hi bob, to admin"
|
||||
where
|
||||
directProhibited :: HasCallStack => TestCC -> IO ()
|
||||
directProhibited cc = do
|
||||
cc <## "alice updated group #team:"
|
||||
cc <## "updated group preferences:"
|
||||
cc <## "Direct messages: off"
|
||||
|
||||
testGroupDirectProhibitNotSupported :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDirectProhibitNotSupported tmp =
|
||||
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = mkVersionRange 1 1} "cath" cathProfile $ \cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
|
||||
bob ##> "#team @cath hi cath"
|
||||
bob <## "peer chat protocol version range incompatible"
|
||||
|
||||
@@ -181,12 +181,7 @@ chatF :: String -> [((Int, String), Maybe String)]
|
||||
chatF = map (\(a, _, c) -> (a, c)) . chat''
|
||||
|
||||
chat'' :: String -> [((Int, String), Maybe (Int, String), Maybe String)]
|
||||
chat'' = map (\(a, b, c) -> (mapNoDirect a, mapNoDirect <$> b, c)) . chat'''
|
||||
where
|
||||
mapNoDirect (a1, _, a3) = (a1, a3)
|
||||
|
||||
chat''' :: String -> [((Int, String, String), Maybe (Int, String, String), Maybe String)]
|
||||
chat''' = read
|
||||
chat'' = read
|
||||
|
||||
chatFeatures :: [(Int, String)]
|
||||
chatFeatures = map (\(a, _, _) -> a) chatFeatures''
|
||||
@@ -461,33 +456,27 @@ showName (TestCC ChatController {currentUser} _ _ _ _ _) = do
|
||||
pure . T.unpack $ localDisplayName <> optionalFullName localDisplayName fullName
|
||||
|
||||
createGroup2 :: HasCallStack => String -> TestCC -> TestCC -> IO ()
|
||||
createGroup2 gName cc1 cc2 = createGroup2' gName cc1 cc2 GRAdmin
|
||||
|
||||
createGroup2' :: HasCallStack => String -> TestCC -> TestCC -> GroupMemberRole -> IO ()
|
||||
createGroup2' gName cc1 cc2 memberRole = do
|
||||
createGroup2 gName cc1 cc2 = do
|
||||
connectUsers cc1 cc2
|
||||
name2 <- userName cc2
|
||||
cc1 ##> ("/g " <> gName)
|
||||
cc1 <## ("group #" <> gName <> " is created")
|
||||
cc1 <## ("to add members use /a " <> gName <> " <name> or /create link #" <> gName)
|
||||
addMember gName cc1 cc2 memberRole
|
||||
addMember gName cc1 cc2 GRAdmin
|
||||
cc2 ##> ("/j " <> gName)
|
||||
concurrently_
|
||||
(cc1 <## ("#" <> gName <> ": " <> name2 <> " joined the group"))
|
||||
(cc2 <## ("#" <> gName <> ": you joined the group"))
|
||||
|
||||
createGroup3 :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> IO ()
|
||||
createGroup3 gName cc1 cc2 cc3 = createGroup3' gName cc1 cc2 cc3 GRAdmin
|
||||
|
||||
createGroup3' :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> GroupMemberRole -> IO ()
|
||||
createGroup3' gName cc1 cc2 cc3 memberRole = do
|
||||
createGroup2' gName cc1 cc2 memberRole
|
||||
createGroup3 gName cc1 cc2 cc3 = do
|
||||
createGroup2 gName cc1 cc2
|
||||
connectUsers cc1 cc3
|
||||
name1 <- userName cc1
|
||||
name3 <- userName cc3
|
||||
sName2 <- showName cc2
|
||||
sName3 <- showName cc3
|
||||
addMember gName cc1 cc3 memberRole
|
||||
addMember gName cc1 cc3 GRAdmin
|
||||
cc3 ##> ("/j " <> gName)
|
||||
concurrentlyN_
|
||||
[ cc1 <## ("#" <> gName <> ": " <> name3 <> " joined the group"),
|
||||
|
||||
@@ -57,7 +57,7 @@ testConnReq = CRInvitationUri connReqData testE2ERatchetParams
|
||||
quotedMsg :: QuotedMsg
|
||||
quotedMsg =
|
||||
QuotedMsg
|
||||
(MsgRef (Just $ SharedMsgId "\5\6\7\8") (systemToUTCTime $ MkSystemTime 1 1) True Nothing Nothing)
|
||||
(MsgRef (Just $ SharedMsgId "\5\6\7\8") (systemToUTCTime $ MkSystemTime 1 1) True Nothing)
|
||||
$ MCText "hello there!"
|
||||
|
||||
(==##) :: MsgEncodingI e => ByteString -> ChatMessage e -> Expectation
|
||||
@@ -105,13 +105,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))
|
||||
it "x.msg.new simple text - timed message TTL" $
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}"
|
||||
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing Nothing))
|
||||
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
|
||||
it "x.msg.new simple text - live message" $
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}"
|
||||
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing Nothing (Just True) Nothing))
|
||||
it "x.msg.new simple text - direct message scope" $
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"scope\":\"direct\"}}"
|
||||
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing Nothing Nothing (Just MSDirect)))
|
||||
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
|
||||
it "x.msg.new simple link" $
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}"
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing}) Nothing))
|
||||
@@ -133,41 +130,27 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
chatInitialVRange
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
(XMsgNew (MCQuote quotedMsg (extMsgContent (MCText "hello to you too") Nothing)))
|
||||
it "x.msg.new quote - direct referenced message scope" $
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\",\"msgScope\":\"direct\"}}}}"
|
||||
##==## ChatMessage
|
||||
chatInitialVRange
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
( XMsgNew
|
||||
( MCQuote
|
||||
( QuotedMsg
|
||||
(MsgRef (Just $ SharedMsgId "\5\6\7\8") (systemToUTCTime $ MkSystemTime 1 1) True Nothing (Just MSDirect))
|
||||
$ MCText "hello there!"
|
||||
)
|
||||
(extMsgContent (MCText "hello to you too") Nothing)
|
||||
)
|
||||
)
|
||||
it "x.msg.new quote - timed message TTL" $
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}"
|
||||
##==## ChatMessage
|
||||
chatInitialVRange
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing (Just 3600) Nothing Nothing)))
|
||||
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing (Just 3600) Nothing)))
|
||||
it "x.msg.new quote - live message" $
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}"
|
||||
##==## ChatMessage
|
||||
chatInitialVRange
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing Nothing (Just True) Nothing)))
|
||||
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing Nothing (Just True))))
|
||||
it "x.msg.new forward" $
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing))
|
||||
it "x.msg.new forward - timed message TTL" $
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing Nothing))
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
|
||||
it "x.msg.new forward - live message" $
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing Nothing (Just True) Nothing))
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
|
||||
it "x.msg.new simple text with file" $
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
|
||||
|
||||
Reference in New Issue
Block a user