mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-16 00:10:56 +00:00
core, ui: invite member of one group to another group (#7682)
* rfc: invite member of one group to another group * update * update * implement * version * update * do not show error if contact was removed when joining group --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny @ SimpleX Chat
parent
1053e9fb5c
commit
07c363849b
@@ -465,6 +465,7 @@ data ChatCommand
|
||||
| APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString}
|
||||
| APIGetConnNtfMessages (NonEmpty ConnMsgReq)
|
||||
| APIAddMember {groupId :: GroupId, contactId :: ContactId, memberRole :: GroupMemberRole}
|
||||
| APIAddMemberViaGroup {groupId :: GroupId, viaGroupId :: GroupId, groupMemberId :: GroupMemberId, memberRole :: GroupMemberRole}
|
||||
| APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter}
|
||||
| APIAcceptMember {groupId :: GroupId, groupMemberId :: GroupMemberId, memberRole :: GroupMemberRole}
|
||||
| APIDeleteMemberSupportChat GroupId GroupMemberId
|
||||
@@ -877,6 +878,7 @@ data ChatResponse
|
||||
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool, msgSigned :: Bool}
|
||||
| CRGroupsList {user :: User, groups :: [GroupInfo]}
|
||||
| CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
|
||||
| CRSentGroupInvitationViaGroup {user :: User, groupInfo :: GroupInfo, member :: GroupMember, viaGroupInfo :: GroupInfo, viaMember :: GroupMember}
|
||||
| CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
|
||||
| CRFileTransferStatusXFTP User AChatItem
|
||||
| CRUserProfile {user :: User, profile :: Profile}
|
||||
@@ -1028,6 +1030,7 @@ data ChatEvent
|
||||
| CEvtHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
||||
| CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
||||
| CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
|
||||
| CEvtReceivedGroupInvitationViaGroup {user :: User, groupInfo :: GroupInfo, viaGroupInfo :: GroupInfo, viaMember :: GroupMember, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
|
||||
| CEvtUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| CEvtGroupLinkDataUpdated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay], relaysChanged :: Bool}
|
||||
| CEvtGroupRelayUpdated {user :: User, groupInfo :: GroupInfo, member :: GroupMember, groupRelay :: GroupRelay}
|
||||
|
||||
@@ -2839,15 +2839,51 @@ processChatCommand cxt nm = \case
|
||||
pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole}
|
||||
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
|
||||
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
|
||||
APIAddMemberViaGroup groupId viaGroupId gmId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do
|
||||
(g@(GIK gInfo _), viaGInfo, m) <- withFastStore $ \db ->
|
||||
(,,) <$> getGroupInfoKeys db cxt user groupId <*> getGroupInfo db cxt user viaGroupId <*> getGroupMember db cxt user viaGroupId gmId
|
||||
let GroupMember {localDisplayName = mName} = m
|
||||
when (viaGroupId == groupId) $ throwChatError $ CEGroupDuplicateMember mName
|
||||
when (useRelays' gInfo) $ throwCmdError "can't invite member to channel"
|
||||
when (useRelays' viaGInfo) $ throwCmdError "can't invite member from channel"
|
||||
assertUserGroupRole gInfo $ max GRAdmin memRole
|
||||
assertUserGroupRole viaGInfo GRAuthor
|
||||
unless (groupFeatureUserAllowed SGFDirectMessages viaGInfo) $ throwCmdError "direct messages not allowed"
|
||||
when (incognitoMembership viaGInfo) $ throwChatError CEContactIncognitoCantInvite
|
||||
when (incognitoMembership gInfo) $ throwChatError CEGroupIncognitoCantInvite
|
||||
mConn <- case memberConn m of
|
||||
Just conn | memberCurrent m -> pure conn
|
||||
_ -> throwChatError CEGroupMemberNotActive
|
||||
unless (m `supportsVersion` groupInvVersion) $ throwChatError CEPeerChatVRangeIncompatible
|
||||
let sendInvitation = sendGrpInvitationViaGroup user viaGInfo m mConn g
|
||||
withFastStore' (\db -> getMemberInvitedVia db cxt user gInfo m) >>= \case
|
||||
Nothing -> do
|
||||
gVar <- asks random
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
(agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff True subMode
|
||||
member <- withFastStore $ \db -> createNewMemberViaGroup db gVar cxt user gInfo m mConn memRole agentConnId cReq subMode
|
||||
sendInvitation member cReq
|
||||
pure $ CRSentGroupInvitationViaGroup user gInfo member viaGInfo m
|
||||
Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole}
|
||||
| memberStatus == GSMemInvited -> do
|
||||
unless (mRole == memRole) $ withFastStore' $ \db -> updateGroupMemberRole db user member memRole
|
||||
withFastStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case
|
||||
Just cReq -> do
|
||||
sendInvitation member {memberRole = memRole} cReq
|
||||
pure $ CRSentGroupInvitationViaGroup user gInfo member {memberRole = memRole} viaGInfo m
|
||||
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo mName
|
||||
| otherwise -> throwChatError $ CEGroupDuplicateMember mName
|
||||
APIJoinGroup groupId enableNtfs -> withUser $ \user@User {userId} -> do
|
||||
withGroupLock "joinGroup" groupId $ do
|
||||
(invitation, ct) <- withFastStore $ \db -> do
|
||||
(invitation, ct_) <- withFastStore $ \db -> do
|
||||
inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db cxt user groupId
|
||||
(inv,) <$> getContactViaMember db cxt user fromMember
|
||||
(inv,) <$> forM (memberContactId fromMember) (\_ -> getContactViaMember db cxt user fromMember)
|
||||
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}, groupKeys = gks} = invitation
|
||||
Contact {activeConn} = ct
|
||||
case activeConn of
|
||||
Just Connection {peerChatVRange} -> do
|
||||
peerChatVRange_ = case ct_ of
|
||||
Nothing -> Right $ memberChatVRange' fromMember
|
||||
Just ct@Contact {activeConn} -> maybe (Left ct) (\Connection {peerChatVRange} -> Right peerChatVRange) activeConn
|
||||
case peerChatVRange_ of
|
||||
Right peerChatVRange -> do
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
dm <- encodeXGrpAcpt user (GIK g gks) peerChatVRange
|
||||
agentConnId <- case memberConn fromMember of
|
||||
@@ -2868,9 +2904,11 @@ processChatCommand cxt nm = \case
|
||||
updateGroupMemberStatus db userId fromMember GSMemInvited
|
||||
updateGroupMemberStatus db userId membership GSMemInvited
|
||||
throwError e
|
||||
updateCIGroupInvitationStatus user g CIGISAccepted `catchAllErrors` eToView
|
||||
updateCIGroupInvitationStatus user g CIGISAccepted `catchAllErrors` \case
|
||||
ChatErrorStore SEChatItemNotFoundByGroupId {} -> pure ()
|
||||
e -> eToView e
|
||||
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
|
||||
Nothing -> throwChatError $ CEContactNotActive ct
|
||||
Left ct -> throwChatError $ CEContactNotActive ct
|
||||
APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do
|
||||
(g@(GIK gInfo _), m) <- withFastStore $ \db -> (,) <$> getGroupInfoKeys db cxt user groupId <*> getGroupMemberById db cxt user gmId
|
||||
-- same rule as role change (moderators grant up to member); pending member's role is a stand-in, so treat it as member
|
||||
@@ -4318,6 +4356,25 @@ processChatCommand cxt nm = \case
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
|
||||
forM_ (timed_ >>= timedDeleteAt') $
|
||||
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci)
|
||||
sendGrpInvitationViaGroup :: User -> GroupInfo -> GroupMember -> Connection -> GroupInfoKeys -> GroupMember -> ConnReqInvitation -> CM ()
|
||||
sendGrpInvitationViaGroup user viaGInfo GroupMember {localDisplayName} mConn (GIK gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} gks) GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
|
||||
let currentMemCount = fromIntegral $ currentMembers $ groupSummary gInfo
|
||||
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
groupInv =
|
||||
GroupInvitation
|
||||
{ fromMember = MemberIdRole userMemberId userRole,
|
||||
fromMemberKey = Just $ groupMemberKey gks,
|
||||
invitedMember = MemberIdRole memberId memRole,
|
||||
connRequest = cReq,
|
||||
groupProfile,
|
||||
business = businessChat,
|
||||
groupLinkId = Nothing,
|
||||
groupSize = Just currentMemCount
|
||||
}
|
||||
(msg, _, _) <- sendDirectMemberMessage mConn (XGrpInv groupInv) (groupId' viaGInfo)
|
||||
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
|
||||
ci <- saveSndChatItem user (CDGroupSnd viaGInfo Nothing) msg content
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat viaGInfo Nothing) ci]
|
||||
addRelays :: User -> GroupInfoKeys -> ShortLinkContact -> [UserChatRelay] -> CM [(UserChatRelay, Either ChatError GroupRelay)]
|
||||
addRelays user g@(GIK gInfo@GroupInfo {membership} gks) groupSLink relays =
|
||||
mapConcurrently addRelay relays
|
||||
@@ -4773,7 +4830,7 @@ processChatCommand cxt nm = \case
|
||||
shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userLinkData Nothing False Nothing)
|
||||
updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM ()
|
||||
updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do
|
||||
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db cxt user groupId
|
||||
AChatItem _ _ cInfo ci@ChatItem {content, meta = meta@CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db cxt user groupId
|
||||
case (cInfo, content) of
|
||||
(DirectChat ct@Contact {contactId}, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole)
|
||||
| status == CIGISPending -> do
|
||||
@@ -4782,6 +4839,14 @@ processChatCommand cxt nm = \case
|
||||
updateDirectChatItemView user ct itemId aciContent False False timed_ Nothing
|
||||
forM_ (timed_ >>= timedDeleteAt') $
|
||||
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, itemId)
|
||||
(GroupChat g _, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole)
|
||||
| status == CIGISPending -> do
|
||||
let content' = CIRcvGroupInvitation (ciGroupInv {status = newStatus} :: CIGroupInvitation) memRole
|
||||
timed_ <- sndGroupCITimed False g Nothing
|
||||
ci' <- withFastStore' $ \db -> updateGroupChatItem db user (groupId' g) ci {meta = meta {itemTimed = timed_}} content' False False Nothing
|
||||
toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci')
|
||||
forM_ (timed_ >>= timedDeleteAt') $
|
||||
startProximateTimedItemThread user (ChatRef CTGroup (groupId' g) Nothing, itemId)
|
||||
_ -> pure () -- prohibited
|
||||
assertAllowedContent :: MsgContent -> CM ()
|
||||
assertAllowedContent = \case
|
||||
@@ -6168,6 +6233,7 @@ chatCommandP =
|
||||
"/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP),
|
||||
"/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP),
|
||||
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
"/_add #" *> (APIAddMemberViaGroup <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
"/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI
|
||||
"/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
"/_delete member chat #" *> (APIDeleteMemberSupportChat <$> A.decimal <* A.space <*> A.decimal),
|
||||
|
||||
@@ -1121,6 +1121,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
XGrpRoster gr -> fmap ctx <$> xGrpRoster gInfo' m'' m'' gr verifiedMsg sharedMsgId_ brokerTs
|
||||
XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck (GIK gInfo' gks) m'' ackVer ackErr
|
||||
XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest (GIK gInfo' gks) m'' reqVer
|
||||
XGrpInv gInv -> Nothing <$ xGrpInv gInfo' m'' conn' gInv msg brokerTs
|
||||
-- TODO [knocking] why don't we forward these messages?
|
||||
XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs
|
||||
XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward (GIK gInfo' gks) Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs
|
||||
@@ -1654,7 +1655,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason
|
||||
xGrpRelayInv :: InvitationId -> VersionRangeChat -> Maybe SignedMsg -> GroupRelayInvitation -> CM ()
|
||||
xGrpRelayInv invId chatVRange signedMsg_ groupRelayInv@GroupRelayInvitation {groupLink}
|
||||
| maxVersion chatVRange >= signedRelayInvVersion && isNothing presHeader_ =
|
||||
| maxVersion chatVRange >= groupInvVersion && isNothing presHeader_ =
|
||||
messageError "x.grp.relay.inv: invitation is not signed by the owner"
|
||||
| otherwise = do
|
||||
rejected <- withStore' $ \db -> isRelayGroupRejected db user groupLink
|
||||
@@ -2703,27 +2704,14 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
| otherwise = do
|
||||
let Contact {localDisplayName = c, activeConn} = ct
|
||||
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
|
||||
forM_ activeConn $ \Connection {connId, connChatVersion, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
|
||||
forM_ activeConn $ \conn@Connection {peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
|
||||
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
|
||||
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
|
||||
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
|
||||
memberKeys <- atomically . C.generateKeyPair =<< asks random
|
||||
(GIK gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership} gks, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId memberKeys
|
||||
(g@(GIK gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership} _), hostId) <- withStore $ \db -> createGroupInvitation db cxt user (Left ct) peerChatVRange inv customUserProfileId memberKeys
|
||||
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
|
||||
let GroupMember {groupMemberId} = membership
|
||||
-- hostContact is only reported for group links, where the client replaces
|
||||
-- the transient host connection view with the group and removes its chat
|
||||
joinGroupAsync hostContact_ sameLink = do
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
dm <- encodeXGrpAcpt user (GIK gInfo gks) peerChatVRange
|
||||
(connIds@(cmdId, acId), _) <- prepareAgentJoin user True connRequest
|
||||
withStore' $ \db -> do
|
||||
when sameLink $ setViaGroupLinkUri db groupId connId
|
||||
createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode
|
||||
updateGroupMemberStatusById db userId hostId GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
joinAgentConnectionAsync cmdId False acId True connRequest dm subMode
|
||||
toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} hostContact_
|
||||
createInvitationItem invStatus = do
|
||||
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = invStatus}) memRole
|
||||
(ci, cInfo) <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content
|
||||
@@ -2731,11 +2719,11 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci]
|
||||
if
|
||||
| sameGroupLinkId groupLinkId groupLinkId' ->
|
||||
joinGroupAsync (Just ct) True
|
||||
joinGroupAsync g hostId conn connRequest (Just ct) True
|
||||
| isTrue (autoAcceptGroupInvitations user) ->
|
||||
-- a resent invitation returns the existing group, so only join while still invited
|
||||
when (memberStatus membership == GSMemInvited) $ do
|
||||
joinGroupAsync Nothing False
|
||||
joinGroupAsync g hostId conn connRequest Nothing False
|
||||
createInvitationItem CIGISAccepted
|
||||
| otherwise -> do
|
||||
createInvitationItem CIGISPending
|
||||
@@ -2747,6 +2735,21 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
sameGroupLinkId (Just gli) (Just gli') = gli == gli'
|
||||
sameGroupLinkId _ _ = False
|
||||
|
||||
-- hostContact is only reported for group links, where the client replaces
|
||||
-- the transient host connection view with the group and removes its chat
|
||||
joinGroupAsync :: GroupInfoKeys -> GroupMemberId -> Connection -> ConnReqInvitation -> Maybe Contact -> Bool -> CM ()
|
||||
joinGroupAsync (GIK gInfo@GroupInfo {groupId, membership} gks) hostId Connection {connId, connChatVersion, peerChatVRange} connRequest hostContact_ sameLink = do
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
dm <- encodeXGrpAcpt user (GIK gInfo gks) peerChatVRange
|
||||
(connIds@(cmdId, acId), _) <- prepareAgentJoin user True connRequest
|
||||
withStore' $ \db -> do
|
||||
when sameLink $ setViaGroupLinkUri db groupId connId
|
||||
createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode
|
||||
updateGroupMemberStatusById db userId hostId GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
joinAgentConnectionAsync cmdId False acId True connRequest dm subMode
|
||||
toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} hostContact_
|
||||
|
||||
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> CM ()
|
||||
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
|
||||
MsgOk -> pure ()
|
||||
@@ -3882,6 +3885,35 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
|
||||
unless (isUserGrpFwdRelay gInfo) $ connectToRelayAsync user gInfo rl
|
||||
pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}
|
||||
|
||||
xGrpInv :: GroupInfo -> GroupMember -> Connection -> GroupInvitation -> RcvMessage -> UTCTime -> CM ()
|
||||
xGrpInv viaGInfo m mConn@Connection {peerChatVRange} inv msg brokerTs
|
||||
| useRelays' viaGInfo = messageError "x.grp.inv: not allowed in channels"
|
||||
| isJust publicGroup = messageError "x.grp.inv: can't invite to channel"
|
||||
| not (groupFeatureMemberAllowed SGFDirectMessages m viaGInfo) = messageError "x.grp.inv: direct messages not allowed"
|
||||
| memberBlocked m = messageWarning "x.grp.inv: member is blocked (ignoring)"
|
||||
| fromRole < GRAdmin || fromRole < memRole = messageError "x.grp.inv: inviting member role"
|
||||
| fromMemId == memId = messageError "x.grp.inv: duplicate member ID"
|
||||
| otherwise = do
|
||||
memberKeys <- atomically . C.generateKeyPair =<< asks random
|
||||
let incognitoProfileId = localProfileId <$> incognitoMembershipProfile viaGInfo
|
||||
(g@(GIK gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership} _), hostId) <- withStore $ \db -> createGroupInvitation db cxt user (Right m) peerChatVRange inv incognitoProfileId memberKeys
|
||||
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
|
||||
let GroupMember {groupMemberId} = membership
|
||||
createInvitationItem invStatus = do
|
||||
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = invStatus}) memRole
|
||||
(ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv viaGInfo Nothing m) msg brokerTs content
|
||||
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
|
||||
groupMsgToView cInfo ci
|
||||
if isTrue (autoAcceptGroupInvitations user)
|
||||
then when (memberStatus membership == GSMemInvited) $ do
|
||||
joinGroupAsync g hostId mConn connRequest Nothing False
|
||||
createInvitationItem CIGISAccepted
|
||||
else do
|
||||
createInvitationItem CIGISPending
|
||||
toView CEvtReceivedGroupInvitationViaGroup {user, groupInfo = gInfo, viaGroupInfo = viaGInfo, viaMember = m, fromMemberRole = fromRole, memberRole = memRole}
|
||||
where
|
||||
GroupInvitation {fromMember = MemberIdRole fromMemId fromRole, invitedMember = MemberIdRole memId memRole, connRequest, groupProfile = GroupProfile {publicGroup}} = inv
|
||||
|
||||
xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> UTCTime -> CM ()
|
||||
xGrpDirectInv g@GroupInfo {groupId, groupProfile = gp} m mConn@Connection {connId = mConnId} connReq mContent_ msg brokerTs
|
||||
| not (groupFeatureMemberAllowed SGFDirectMessages m g) = messageError "x.grp.direct.inv: direct messages not allowed"
|
||||
|
||||
@@ -87,6 +87,8 @@ import Simplex.Messaging.Version hiding (version)
|
||||
-- 18 - relay web capabilities (2026-05-31)
|
||||
-- 19 - group roster (2026-06-18)
|
||||
-- 20 - p2p group member keys for signing (2026-07-26)
|
||||
-- 21 - command keywords with any characters (2026-09-27)
|
||||
-- 22 - owner-signed relay invitations, group invitations via member connections (2026-10-10)
|
||||
|
||||
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
|
||||
-- This indirection is needed for backward/forward compatibility testing.
|
||||
@@ -143,8 +145,8 @@ groupMemberKeyVersion = VersionChat 20
|
||||
anyTextCommandsVersion :: VersionChat
|
||||
anyTextCommandsVersion = VersionChat 21
|
||||
|
||||
signedRelayInvVersion :: VersionChat
|
||||
signedRelayInvVersion = VersionChat 22
|
||||
groupInvVersion :: VersionChat
|
||||
groupInvVersion = VersionChat 22
|
||||
|
||||
data ConnectionEntity
|
||||
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
||||
|
||||
@@ -86,6 +86,7 @@ module Simplex.Chat.Store.Groups
|
||||
getContactGroupPreferences,
|
||||
getGroupInvitation,
|
||||
createNewContactMember,
|
||||
createNewMemberViaGroup,
|
||||
createGroupRelayRecord,
|
||||
getGroupRelayById,
|
||||
getGroupRelayByGMId,
|
||||
@@ -140,6 +141,7 @@ module Simplex.Chat.Store.Groups
|
||||
createBusinessRequestGroup,
|
||||
getContactViaMember,
|
||||
getMemberInvitation,
|
||||
getMemberInvitedVia,
|
||||
createMemberConnection,
|
||||
createMemberConnectionAsync,
|
||||
updatePreparedRelayedGroup,
|
||||
@@ -455,9 +457,8 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile memberId
|
||||
}
|
||||
|
||||
-- | creates a new group record for the group the current user was invited to, or returns an existing one
|
||||
createGroupInvitation :: DB.Connection -> StoreCxt -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> C.KeyPairEd25519 -> ExceptT StoreError IO (GroupInfoKeys, GroupMemberId)
|
||||
createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ = throwError $ SEContactNotReady localDisplayName
|
||||
createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, activeConn = Just Connection {peerChatVRange}} GroupInvitation {fromMember, fromMemberKey, invitedMember, connRequest, groupProfile, business} incognitoProfileId memberKeys = do
|
||||
createGroupInvitation :: DB.Connection -> StoreCxt -> User -> Either Contact GroupMember -> VersionRangeChat -> GroupInvitation -> Maybe ProfileId -> C.KeyPairEd25519 -> ExceptT StoreError IO (GroupInfoKeys, GroupMemberId)
|
||||
createGroupInvitation db cxt user@User {userId} inviter peerChatVRange GroupInvitation {fromMember, fromMemberKey, invitedMember, connRequest, groupProfile, business} incognitoProfileId memberKeys = do
|
||||
liftIO getInvitationGroupId_ >>= \case
|
||||
Nothing -> createGroupInvitation_
|
||||
Just gId -> do
|
||||
@@ -501,8 +502,28 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti
|
||||
((profileId, localDisplayName, connRequest, userId, BI True, currentTs, currentTs, currentTs, currentTs, snd memberKeys) :. businessChatInfoRow business)
|
||||
insertedRowId db
|
||||
let hostVRange = adjustedMemberVRange (vr cxt) peerChatVRange
|
||||
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing ((\(MemberKey k) -> k) <$> fromMemberKey) currentTs hostVRange
|
||||
membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId (Just $ fst memberKeys) currentTs (vr cxt)
|
||||
(groupMemberId, invitedBy) <- case inviter of
|
||||
Left contact@Contact {contactId} -> do
|
||||
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing ((\(MemberKey k) -> k) <$> fromMemberKey) currentTs hostVRange
|
||||
pure (groupMemberId, IBContact contactId)
|
||||
Right GroupMember {memberProfile} -> do
|
||||
(hostName, memProfileId, hostProfile, badgeVerified) <- createNewMemberProfile_ db cxt user (fromLocalProfile memberProfile) Nothing currentTs
|
||||
let MemberIdRole {memberId, memberRole} = fromMember
|
||||
host =
|
||||
NewGroupMember
|
||||
{ memInfo = MemberInfo memberId memberRole (Just $ ChatVersionRange hostVRange) hostProfile fromMemberKey,
|
||||
memCategory = GCHostMember,
|
||||
memStatus = GSMemInvited,
|
||||
memRestriction = Nothing,
|
||||
memInvitedBy = IBUnknown,
|
||||
memInvitedByGroupMemberId = Nothing,
|
||||
localDisplayName = hostName,
|
||||
memContactId = Nothing,
|
||||
memProfileId
|
||||
}
|
||||
GroupMember {groupMemberId} <- createNewMember_ db user groupId host badgeVerified currentTs
|
||||
pure (groupMemberId, IBUnknown)
|
||||
membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited invitedBy incognitoProfileId (Just $ fst memberKeys) currentTs (vr cxt)
|
||||
let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
|
||||
pure
|
||||
( GroupInfo
|
||||
@@ -1436,6 +1457,32 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId,
|
||||
)
|
||||
pure indexInGroup
|
||||
|
||||
createNewMemberViaGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupInfo -> GroupMember -> Connection -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
||||
createNewMemberViaGroup db gVar cxt user@User {userId} GroupInfo {groupId, membership} viaMember@GroupMember {memberProfile} Connection {connChatVersion, peerChatVRange} memberRole agentConnId connRequest subMode =
|
||||
createWithRandomId' db gVar $ \memId -> runExceptT $ do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
(localDisplayName, memProfileId, profile, badgeVerified) <- createNewMemberProfile_ db cxt user (fromLocalProfile memberProfile) Nothing createdAt
|
||||
let newMember =
|
||||
NewGroupMember
|
||||
{ memInfo = MemberInfo (MemberId memId) memberRole (Just $ ChatVersionRange peerChatVRange) profile Nothing,
|
||||
memCategory = GCInviteeMember,
|
||||
memStatus = GSMemInvited,
|
||||
memRestriction = Nothing,
|
||||
memInvitedBy = IBUser,
|
||||
memInvitedByGroupMemberId = Just $ groupMemberId' membership,
|
||||
localDisplayName,
|
||||
memContactId = Nothing,
|
||||
memProfileId
|
||||
}
|
||||
member@GroupMember {groupMemberId} <- createNewMember_ db user groupId newMember badgeVerified createdAt
|
||||
liftIO $ do
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE group_members SET sent_inv_queue_info = ?, invited_via_group_member_id = ? WHERE group_member_id = ?"
|
||||
(connRequest, groupMemberId' viaMember, groupMemberId)
|
||||
void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVersion peerChatVRange Nothing 0 createdAt subMode
|
||||
pure member
|
||||
|
||||
createGroupRelayRecord :: DB.Connection -> GroupInfo -> GroupMember -> UserChatRelay -> ExceptT StoreError IO GroupRelay
|
||||
createGroupRelayRecord db GroupInfo {groupId} GroupMember {groupMemberId} UserChatRelay {chatRelayId} = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
@@ -2239,6 +2286,15 @@ getMemberInvitation db User {userId} groupMemberId =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
|
||||
|
||||
getMemberInvitedVia :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMember -> IO (Maybe GroupMember)
|
||||
getMemberInvitedVia db cxt user@User {userId} GroupInfo {groupId} viaMember = do
|
||||
currentTs <- getCurrentTime
|
||||
maybeFirstRow (toContactMember currentTs cxt user) $
|
||||
DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND m.invited_via_group_member_id = ? AND m.member_status NOT IN (?,?,?)")
|
||||
(userId, groupId, groupMemberId' viaMember, GSMemRejected, GSMemRemoved, GSMemLeft)
|
||||
|
||||
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO Connection
|
||||
createMemberConnection db userId GroupMember {groupMemberId} agentConnId chatV peerChatVRange subMode = do
|
||||
currentTs <- getCurrentTime
|
||||
@@ -2441,7 +2497,7 @@ createNewGroupMember db cxt user gInfo invitingMember memInfo@MemberInfo {profil
|
||||
memContactId = Nothing,
|
||||
memProfileId
|
||||
}
|
||||
createNewMember_ db user gInfo newMember badgeVerified currentTs
|
||||
createNewMember_ db user (groupId' gInfo) newMember badgeVerified currentTs
|
||||
|
||||
createNewMemberProfile_ :: DB.Connection -> StoreCxt -> User -> Profile -> Maybe ProofPresHeader -> UTCTime -> ExceptT StoreError IO (Text, ProfileId, Profile, Maybe Bool)
|
||||
createNewMemberProfile_ db cxt User {userId} p@Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences} presHeader_ createdAt =
|
||||
@@ -2454,11 +2510,11 @@ createNewMemberProfile_ db cxt User {userId} p@Profile {displayName, fullName, s
|
||||
profileId <- insertedRowId db
|
||||
pure $ Right (ldn, profileId, p', badgeVerified)
|
||||
|
||||
createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> Maybe Bool -> UTCTime -> ExceptT StoreError IO GroupMember
|
||||
createNewMember_ :: DB.Connection -> User -> GroupId -> NewGroupMember -> Maybe Bool -> UTCTime -> ExceptT StoreError IO GroupMember
|
||||
createNewMember_
|
||||
db
|
||||
User {userId, userContactId}
|
||||
GroupInfo {groupId}
|
||||
groupId
|
||||
NewGroupMember
|
||||
{ memInfo = MemberInfo memberId memberRole memChatVRange memberProfile@Profile {badge} memKey,
|
||||
memCategory = memberCategory,
|
||||
@@ -2639,7 +2695,7 @@ createIntroReMember
|
||||
(localDisplayName, memProfileId, memberProfile', badgeVerified) <- createNewMemberProfile_ db cxt user memberProfile presHeader_ currentTs
|
||||
let memRestriction = restriction <$> memRestrictions_
|
||||
newMember = NewGroupMember {memInfo = (memInfo :: MemberInfo) {profile = memberProfile'}, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId}
|
||||
createNewMember_ db user gInfo newMember badgeVerified currentTs
|
||||
createNewMember_ db user (groupId' gInfo) newMember badgeVerified currentTs
|
||||
|
||||
createIntroReMemberConn :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionChat -> MemberInfo -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
||||
createIntroReMemberConn
|
||||
|
||||
@@ -54,6 +54,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260918_badge_issue_errors
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260925_badge_bindings
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20261010_invite_member_via_group
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -107,7 +108,8 @@ schemaMigrations =
|
||||
("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges),
|
||||
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors),
|
||||
("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json),
|
||||
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings)
|
||||
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings),
|
||||
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20261010_invite_member_via_group where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20261010_invite_member_via_group :: Text
|
||||
m20261010_invite_member_via_group =
|
||||
[r|
|
||||
ALTER TABLE group_members ADD COLUMN invited_via_group_member_id BIGINT REFERENCES group_members(group_member_id) ON DELETE SET NULL;
|
||||
CREATE INDEX idx_group_members_invited_via_group_member_id ON group_members(invited_via_group_member_id);
|
||||
|]
|
||||
|
||||
down_m20261010_invite_member_via_group :: Text
|
||||
down_m20261010_invite_member_via_group =
|
||||
[r|
|
||||
DROP INDEX idx_group_members_invited_via_group_member_id;
|
||||
ALTER TABLE group_members DROP COLUMN invited_via_group_member_id;
|
||||
|]
|
||||
@@ -1022,7 +1022,8 @@ CREATE TABLE test_chat_schema.group_members (
|
||||
removed_at timestamp with time zone,
|
||||
roster_served_version bigint,
|
||||
member_security_code text,
|
||||
member_security_code_verified_at timestamp with time zone
|
||||
member_security_code_verified_at timestamp with time zone,
|
||||
invited_via_group_member_id bigint
|
||||
);
|
||||
|
||||
|
||||
@@ -2709,6 +2710,10 @@ CREATE INDEX idx_group_members_invited_by_group_member_id ON test_chat_schema.gr
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_group_members_invited_via_group_member_id ON test_chat_schema.group_members USING btree (invited_via_group_member_id);
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_group_members_member_profile_id ON test_chat_schema.group_members USING btree (member_profile_id);
|
||||
|
||||
|
||||
@@ -3465,6 +3470,11 @@ ALTER TABLE ONLY test_chat_schema.group_members
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.group_members
|
||||
ADD CONSTRAINT group_members_invited_via_group_member_id_fkey FOREIGN KEY (invited_via_group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE SET NULL;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.group_members
|
||||
ADD CONSTRAINT group_members_member_profile_id_fkey FOREIGN KEY (member_profile_id) REFERENCES test_chat_schema.contact_profiles(contact_profile_id) ON DELETE SET NULL;
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260918_badge_issue_errors
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260925_badge_bindings
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20261010_invite_member_via_group
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -353,7 +354,8 @@ schemaMigrations =
|
||||
("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges),
|
||||
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors),
|
||||
("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json),
|
||||
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings)
|
||||
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings),
|
||||
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20261010_invite_member_via_group where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20261010_invite_member_via_group :: Query
|
||||
m20261010_invite_member_via_group =
|
||||
[sql|
|
||||
ALTER TABLE group_members ADD COLUMN invited_via_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE SET NULL;
|
||||
CREATE INDEX idx_group_members_invited_via_group_member_id ON group_members(invited_via_group_member_id);
|
||||
|]
|
||||
|
||||
down_m20261010_invite_member_via_group :: Query
|
||||
down_m20261010_invite_member_via_group =
|
||||
[sql|
|
||||
DROP INDEX idx_group_members_invited_via_group_member_id;
|
||||
ALTER TABLE group_members DROP COLUMN invited_via_group_member_id;
|
||||
|]
|
||||
@@ -55,6 +55,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -110,6 +111,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -322,6 +324,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -359,6 +362,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -396,6 +400,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -621,6 +626,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -657,6 +663,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -694,6 +701,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -731,6 +739,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -1304,6 +1313,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -1342,6 +1352,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -1997,6 +2008,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -2034,6 +2046,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -6235,6 +6248,29 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
|
||||
SEARCH bp USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
|
||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json,
|
||||
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
|
||||
m.created_at, m.updated_at,
|
||||
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version,
|
||||
bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
LEFT JOIN connections c ON c.group_member_id = m.group_member_id
|
||||
LEFT JOIN group_member_badge_proofs bp ON bp.group_member_id = m.group_member_id
|
||||
WHERE m.user_id = ? AND m.group_id = ? AND m.invited_via_group_member_id = ? AND m.member_status NOT IN (?,?,?)
|
||||
Plan:
|
||||
SEARCH m USING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
|
||||
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
|
||||
SEARCH bp USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
|
||||
|
||||
Query:
|
||||
SELECT
|
||||
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
|
||||
@@ -7006,6 +7042,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -7038,6 +7075,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -7070,6 +7108,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
|
||||
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
|
||||
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
|
||||
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
|
||||
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
|
||||
@@ -7603,7 +7642,7 @@ SCAN files
|
||||
|
||||
Query: SELECT count(1) FROM group_members
|
||||
Plan:
|
||||
SCAN group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id
|
||||
SCAN group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id
|
||||
|
||||
Query: SELECT count(1) FROM pending_group_messages
|
||||
Plan:
|
||||
@@ -7703,6 +7742,10 @@ Query: SELECT issue_failed_since, issue_error FROM badge_purchases
|
||||
Plan:
|
||||
SCAN badge_purchases
|
||||
|
||||
Query: SELECT item_content FROM chat_items WHERE group_id = 1 ORDER BY chat_item_id DESC LIMIT 1
|
||||
Plan:
|
||||
SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?)
|
||||
|
||||
Query: SELECT last_insert_rowid()
|
||||
Plan:
|
||||
SCAN CONSTANT ROW
|
||||
@@ -8183,6 +8226,10 @@ Query: UPDATE group_members SET roster_served_version = ?, updated_at = ? WHERE
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE group_members SET sent_inv_queue_info = ?, invited_via_group_member_id = ? WHERE group_member_id = ?
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE group_members SET support_chat_items_member_attention = ?, updated_at = ? WHERE group_member_id = ?
|
||||
Plan:
|
||||
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
@@ -260,6 +260,7 @@ CREATE TABLE group_members(
|
||||
roster_served_version INTEGER,
|
||||
member_security_code TEXT,
|
||||
member_security_code_verified_at TEXT,
|
||||
invited_via_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE SET NULL,
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON DELETE CASCADE
|
||||
@@ -1557,6 +1558,9 @@ CREATE INDEX idx_badge_code_redemptions_user ON badge_code_redemptions(
|
||||
CREATE UNIQUE INDEX idx_badge_purchases_code_redemption ON badge_purchases(
|
||||
badge_code_redemption_id
|
||||
);
|
||||
CREATE INDEX idx_group_members_invited_via_group_member_id ON group_members(
|
||||
invited_via_group_member_id
|
||||
);
|
||||
CREATE TRIGGER on_group_members_insert_update_summary
|
||||
AFTER INSERT ON group_members
|
||||
FOR EACH ROW
|
||||
|
||||
@@ -82,6 +82,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
|
||||
CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c
|
||||
CRGroupDeletedUser u g _ _ -> whenCurrUser cc u $ unsetActiveGroup ct g
|
||||
CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g
|
||||
CRSentGroupInvitationViaGroup u g _ _ _ -> whenCurrUser cc u $ setActiveGroup ct g
|
||||
CRCmdOk _ -> case cmd of
|
||||
Right APIDeleteUser {} -> setActive ct ""
|
||||
_ -> pure ()
|
||||
|
||||
@@ -198,6 +198,9 @@ chatEventNotification t@ChatTerminal {sendNotification} cc = \case
|
||||
CEvtReceivedGroupInvitation u g ct _ _ ->
|
||||
when (contactNtf u ct False) $
|
||||
sendNtf ("#" <> viewGroupName g <> " " <> viewContactName ct <> "> ", "invited you to join the group")
|
||||
CEvtReceivedGroupInvitationViaGroup u g viaG m _ _ ->
|
||||
when (groupNtf u viaG False) $
|
||||
sendNtf ("#" <> viewGroupName g <> " " <> viewMemberName m <> "> ", "invited you to join the group")
|
||||
CEvtUserJoinedGroup u g _ -> when (groupNtf u g False) $ do
|
||||
whenCurrUser cc u $ setActiveGroup t g
|
||||
sendNtf ("#" <> viewGroupName g, "you are connected to group")
|
||||
|
||||
@@ -208,6 +208,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
|
||||
-- CRGroupConversationsDeleted u _g _conversations -> ttyUser u []
|
||||
CRGroupsList u gs -> ttyUser u $ viewGroupsList gs
|
||||
CRSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c
|
||||
CRSentGroupInvitationViaGroup u g _ viaG viaM -> ttyUser u ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyMember viaM <> " in " <> ttyGroup' viaG]
|
||||
CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus
|
||||
CRFileTransferStatusXFTP u ci -> ttyUser u $ viewFileTransferStatusXFTP ci
|
||||
CRUserProfile u@User {profile = LocalProfile {localBadge}} p -> ttyUser u $ viewUserProfile localBadge p
|
||||
@@ -514,7 +515,8 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
|
||||
in ttyUser u [sShow connId <> ": END"]
|
||||
CEvtSubscriptionStatus srv status conns -> [plain $ subStatusStr status <> " " <> tshow (length conns) <> " connections on server " <> showSMPServer srv]
|
||||
CEvtServiceSubStatus srv event -> [plain $ serviceSubEventStr srv event]
|
||||
CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
|
||||
CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g (ttyContact' c) r
|
||||
CEvtReceivedGroupInvitationViaGroup {user = u, groupInfo = g, viaGroupInfo = viaG, viaMember = m, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g (ttyMember m <> " in " <> ttyGroup' viaG) r
|
||||
CEvtUserJoinedGroup u g m -> ttyUser u $ viewUserJoinedGroup g m
|
||||
CEvtGroupLinkDataUpdated u g groupLink relays relaysChanged
|
||||
| relaysChanged -> ttyUser u $ viewGroupLinkRelaysUpdated g groupLink relays
|
||||
@@ -732,7 +734,6 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa
|
||||
GroupChat g scopeInfo -> case chatDir of
|
||||
CIGroupSnd -> case content of
|
||||
CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc
|
||||
CISndGroupInvitation {} -> showSndItemProhibited to
|
||||
_ -> showSndItem to
|
||||
where
|
||||
to = ttyToGroup g scopeInfo
|
||||
@@ -743,7 +744,6 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa
|
||||
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
|
||||
CIRcvMsgError err -> viewRcvMsgError from err ts tz meta
|
||||
CIRcvGroupInvitation {} | isJust m_ -> showRcvItemProhibited from
|
||||
CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False
|
||||
CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False
|
||||
_ -> showRcvItem from
|
||||
@@ -1402,9 +1402,9 @@ viewConnectedToGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, m
|
||||
]
|
||||
_ -> [ttyGroup' g <> ": " <> connectedMember m <> " is connected"]
|
||||
|
||||
viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString]
|
||||
viewReceivedGroupInvitation g c role =
|
||||
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> showRole role
|
||||
viewReceivedGroupInvitation :: GroupInfo -> StyledString -> GroupMemberRole -> [StyledString]
|
||||
viewReceivedGroupInvitation g inviter role =
|
||||
ttyFullGroup g <> ": " <> inviter <> " invites you to join the group as " <> showRole role
|
||||
: case incognitoMembershipProfile g of
|
||||
Just mp -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)]
|
||||
Nothing -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to accept"]
|
||||
|
||||
Reference in New Issue
Block a user