core: prepare and connect to group (#5964)

This commit is contained in:
spaced4ndy
2025-06-05 13:16:04 +00:00
committed by GitHub
parent fb5ce65597
commit 56c8d8696e
22 changed files with 486 additions and 133 deletions
+3
View File
@@ -746,6 +746,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case sentConfirmation(user: UserRef, connection: PendingContactConnection)
case sentInvitation(user: UserRef, connection: PendingContactConnection)
case startedConnectionToContact(user: UserRef, contact: Contact)
case startedConnectionToGroup(user: UserRef, groupInfo: GroupInfo)
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
case contactAlreadyExists(user: UserRef, contact: Contact)
case contactDeleted(user: UserRef, contact: Contact)
@@ -788,6 +789,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case .sentConfirmation: "sentConfirmation"
case .sentInvitation: "sentInvitation"
case .startedConnectionToContact: "startedConnectionToContact"
case .startedConnectionToGroup: "startedConnectionToGroup"
case .sentInvitationToContact: "sentInvitationToContact"
case .contactAlreadyExists: "contactAlreadyExists"
case .contactDeleted: "contactDeleted"
@@ -866,6 +868,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
case let .startedConnectionToContact(u, contact): return withUser(u, String(describing: contact))
case let .startedConnectionToGroup(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
}
+1 -1
View File
@@ -1025,7 +1025,7 @@ func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgConten
func apiConnectPreparedGroup(groupId: Int64, incognito: Bool) async throws -> GroupInfo {
let r: ChatResponse1 = try await chatSendCmd(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito))
// if case let .startedConnectionToGroup(_, groupInfo) = r { return groupInfo } // TODO [short links] startedConnectionToGroup response
if case let .startedConnectionToGroup(_, groupInfo) = r { return groupInfo }
throw r.unexpected
}
+6 -2
View File
@@ -117,6 +117,10 @@ struct ChatView: View {
connectingText()
if selectedChatItems == nil {
let reason = chat.chatInfo.userCantSendReason
let composeEnabled = (
chat.chatInfo.sendMsgEnabled ||
(chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) // allow to join prepared group without message
)
ComposeView(
chat: chat,
im: im,
@@ -126,8 +130,8 @@ struct ChatView: View {
selectedRange: $selectedRange,
disabledText: reason?.composeLabel
)
.disabled(!cInfo.sendMsgEnabled)
.if(!cInfo.sendMsgEnabled) { v in
.disabled(!composeEnabled)
.if(!composeEnabled) { v in
v.disabled(true).onTapGesture {
AlertManager.shared.showAlertMsg(
title: "You can't send messages!",
@@ -389,14 +389,14 @@ struct ComposeView: View {
default: previewView()
}
HStack (alignment: .bottom) {
if !(chat.chatInfo.contact?.sendMsgToConnect ?? false) {
if !chat.chatInfo.nextConnect {
attachmentButton()
}
sendMessageView(simplexLinkProhibited, fileProhibited, voiceProhibited)
if chat.chatInfo.contact?.sendMsgToConnect ?? false {
sendToConnectButton()
if chat.chatInfo.nextConnect {
nextConnectButton()
.padding(.bottom, 16)
.padding(.horizontal, 8)
}
@@ -581,7 +581,7 @@ struct ComposeView: View {
composeState.liveMessage = nil
chatModel.removeLiveDummy()
},
showComposeActionButtons: !(chat.chatInfo.contact?.sendMsgToConnect ?? false),
showComposeActionButtons: !chat.chatInfo.nextConnect,
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited,
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
@@ -636,23 +636,37 @@ struct ComposeView: View {
}
}
private func sendToConnectButton() -> some View {
@ViewBuilder private func nextConnectButton() -> some View {
let connectButtonEnabled = (
composeState.sendEnabled ||
(chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) // allow to join prepared group without message
)
Button {
Task {
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
await sendMemberContactInvitation()
} else if chat.chatInfo.contact?.nextConnectPrepared ?? false {
await sendConnectPreparedContact()
} else if chat.chatInfo.groupInfo?.nextConnectPrepared ?? false {
await connectPreparedGroup()
}
}
} label: {
HStack {
Text("Connect")
.fontWeight(.medium)
Image(systemName: "person.fill.badge.plus")
if case .group = chat.chatInfo {
HStack {
Text("Join")
.fontWeight(.medium)
Image(systemName: "person.2.fill")
}
} else {
HStack {
Text("Connect")
.fontWeight(.medium)
Image(systemName: "person.fill.badge.plus")
}
}
}
.disabled(!composeState.sendEnabled)
.disabled(!connectButtonEnabled)
}
private func sendMemberContactInvitation() async {
@@ -684,6 +698,20 @@ struct ComposeView: View {
}
}
private func connectPreparedGroup() async {
do {
// TODO [short links] allow to choose incognito, different user profile (as "compose context")
let groupInfo = try await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: false)
await MainActor.run {
self.chatModel.updateGroup(groupInfo)
clearState()
}
} catch {
logger.error("ChatView.connectPreparedGroup error: \(error.localizedDescription)")
AlertManager.shared.showAlertMsg(title: "Error joining group", message: "Error: \(responseError(error))")
}
}
private func checkLinkPreview() -> MsgContent {
let msgText = composeState.message
switch (composeState.preview) {
@@ -140,36 +140,38 @@ struct GroupChatInfoView: View {
} footer: {
Text("Delete chat messages from your device.")
}
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
if groupInfo.canAddMembers {
if (chat.chatInfo.incognito) {
Label("Invite members", systemImage: "plus")
.foregroundColor(Color(uiColor: .tertiaryLabel))
.onTapGesture { alert = .cantInviteIncognitoAlert }
} else {
addMembersButton()
if !groupInfo.nextConnectPrepared {
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
if groupInfo.canAddMembers {
if (chat.chatInfo.incognito) {
Label("Invite members", systemImage: "plus")
.foregroundColor(Color(uiColor: .tertiaryLabel))
.onTapGesture { alert = .cantInviteIncognitoAlert }
} else {
addMembersButton()
}
}
}
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
.padding(.leading, 8)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let filteredMembers = s == ""
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
.padding(.leading, 8)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let filteredMembers = s == ""
? members
: members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
MemberRowView(
chat: chat,
groupInfo: groupInfo,
groupMember: GMember(groupInfo.membership),
scrollToItemId: $scrollToItemId,
user: true,
alert: $alert
)
ForEach(filteredMembers) { member in
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert)
MemberRowView(
chat: chat,
groupInfo: groupInfo,
groupMember: GMember(groupInfo.membership),
scrollToItemId: $scrollToItemId,
user: true,
alert: $alert
)
ForEach(filteredMembers) { member in
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert)
}
}
}
Section {
clearChatButton()
if groupInfo.canDelete {
@@ -340,12 +340,16 @@ struct ChatPreviewView: View {
chatPreviewInfoText("connecting…")
}
case let .group(groupInfo, _):
switch (groupInfo.membership.memberStatus) {
case .memRejected: chatPreviewInfoText("rejected")
case .memInvited: groupInvitationPreviewText(groupInfo)
case .memAccepted: chatPreviewInfoText("connecting…")
case .memPendingReview, .memPendingApproval: chatPreviewInfoText("reviewed by admins")
default: EmptyView()
if groupInfo.nextConnectPrepared {
chatPreviewInfoText("open to join")
} else {
switch (groupInfo.membership.memberStatus) {
case .memRejected: chatPreviewInfoText("rejected")
case .memInvited: groupInvitationPreviewText(groupInfo)
case .memAccepted: chatPreviewInfoText("connecting…")
case .memPendingReview, .memPendingApproval: chatPreviewInfoText("reviewed by admins")
default: EmptyView()
}
}
default: EmptyView()
}
@@ -1025,9 +1025,49 @@ private func showPrepareContactAlert(
await MainActor.run {
ChatModel.shared.addChat(Chat(chatInfo: .direct(contact: contact)))
openKnownContact(contact, dismiss: dismiss, showAlreadyExistsAlert: nil)
cleanup?()
}
} catch let error {
logger.error("deleteGroupAlert apiDeleteChat error: \(error.localizedDescription)")
logger.error("showPrepareContactAlert apiPrepareContact error: \(error.localizedDescription)")
showAlert(NSLocalizedString("Error preparing contact", comment: ""), message: responseError(error))
await MainActor.run {
cleanup?()
}
}
}
}
)
}
private func showPrepareGroupAlert(
connectionLink: CreatedConnLink,
groupShortLinkData: GroupShortLinkData,
theme: AppTheme,
dismiss: Bool,
cleanup: (() -> Void)?
) {
showOpenChatAlert(
profileName: groupShortLinkData.groupProfile.displayName,
profileImage: ProfileImage(imageStr: groupShortLinkData.groupProfile.image, iconName: "person.2.circle.fill", size: 60),
theme: theme,
cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"),
confirmTitle: NSLocalizedString("Open chat", comment: "new chat action"),
onCancel: { cleanup?() },
onConfirm: {
Task {
do {
let groupInfo = try await apiPrepareGroup(connLink: connectionLink, groupShortLinkData: groupShortLinkData)
await MainActor.run {
ChatModel.shared.addChat(Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil)))
openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil)
cleanup?()
}
} catch let error {
logger.error("showPrepareGroupAlert apiPrepareGroup error: \(error.localizedDescription)")
showAlert(NSLocalizedString("Error preparing group", comment: ""), message: responseError(error))
await MainActor.run {
cleanup?()
}
}
}
}
@@ -1050,7 +1090,7 @@ func planAndConnect(
switch ilp {
case let .ok(contactSLinkData_):
if let contactSLinkData = contactSLinkData_ {
logger.debug("planAndConnect, .invitationLink, .ok, no short link data")
logger.debug("planAndConnect, .invitationLink, .ok, short link data present")
await MainActor.run {
showPrepareContactAlert(
connectionLink: connectionLink,
@@ -1061,7 +1101,7 @@ func planAndConnect(
)
}
} else {
logger.debug("planAndConnect, .invitationLink, .ok, short link data present")
logger.debug("planAndConnect, .invitationLink, .ok, no short link data")
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"),
@@ -1111,7 +1151,7 @@ func planAndConnect(
switch cap {
case let .ok(contactSLinkData_):
if let contactSLinkData = contactSLinkData_ {
logger.debug("planAndConnect, .contactAddress, .ok, no short link data")
logger.debug("planAndConnect, .contactAddress, .ok, short link data present")
await MainActor.run {
showPrepareContactAlert(
connectionLink: connectionLink,
@@ -1122,7 +1162,7 @@ func planAndConnect(
)
}
} else {
logger.debug("planAndConnect, .contactAddress, .ok, short link data present")
logger.debug("planAndConnect, .contactAddress, .ok, no short link data")
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"),
@@ -1189,10 +1229,18 @@ func planAndConnect(
switch glp {
case let .ok(groupSLinkData_):
if let groupSLinkData = groupSLinkData_ {
logger.debug("planAndConnect, .groupLink, .ok, no short link data")
// TODO [short links] showPrepareGroupAlert -> apiPrepareGroup
} else {
logger.debug("planAndConnect, .groupLink, .ok, short link data present")
await MainActor.run {
showPrepareGroupAlert(
connectionLink: connectionLink,
groupShortLinkData: groupSLinkData,
theme: theme,
dismiss: dismiss,
cleanup: cleanup
)
}
} else {
logger.debug("planAndConnect, .groupLink, .ok, no short link data")
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Join group", comment: "new chat sheet title"),
+2
View File
@@ -685,6 +685,7 @@ public enum ChatErrorType: Decodable, Hashable {
case invalidConnReq
case unsupportedConnReq
case invalidChatMessage(connection: Connection, message: String)
case connReqMessageProhibited
case contactNotReady(contact: Contact)
case contactNotActive(contact: Contact)
case contactDisabled(contact: Contact)
@@ -761,6 +762,7 @@ public enum StoreError: Decodable, Hashable {
case groupNotFoundByName(groupName: GroupName)
case groupMemberNameNotFound(groupId: Int64, groupMemberName: ContactName)
case groupMemberNotFound(groupMemberId: Int64)
case groupHostMemberNotFound(groupId: Int64)
case groupMemberNotFoundByMemberId(memberId: String)
case memberContactGroupMemberNotFound(contactId: Int64)
case groupWithoutUser
+14 -1
View File
@@ -1326,7 +1326,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
}
}
}
public var chatDeleted: Bool {
get {
switch self {
@@ -1336,6 +1336,16 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
}
}
public var nextConnect: Bool {
get {
switch self {
case let .direct(contact): return contact.sendMsgToConnect
case let .group(groupInfo, _): return groupInfo.nextConnectPrepared
default: return false
}
}
}
public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? {
get {
switch self {
@@ -2056,12 +2066,14 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
var updatedAt: Date
var chatTs: Date?
public var connLinkToConnect: CreatedConnLink?
public var connLinkStartedConnection: Bool
public var uiThemes: ThemeModeOverrides?
public var membersRequireAttention: Int
public var id: ChatId { get { "#\(groupId)" } }
public var apiId: Int64 { get { groupId } }
public var ready: Bool { get { true } }
public var nextConnectPrepared: Bool { get { connLinkToConnect != nil && !connLinkStartedConnection } }
public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias }
public var fullName: String { get { groupProfile.fullName } }
public var image: String? { get { groupProfile.image } }
@@ -2094,6 +2106,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
chatSettings: ChatSettings.defaults,
createdAt: .now,
updatedAt: .now,
connLinkStartedConnection: false,
membersRequireAttention: 0,
chatTags: [],
localAlias: ""
+1
View File
@@ -688,6 +688,7 @@ data ChatResponse
| CRSentConfirmation {user :: User, connection :: PendingContactConnection}
| CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile}
| CRStartedConnectionToContact {user :: User, contact :: Contact}
| CRStartedConnectionToGroup {user :: User, groupInfo :: GroupInfo}
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
| CRItemsReadForChat {user :: User, chatInfo :: AChatInfo}
| CRContactDeleted {user :: User, contact :: Contact}
+39 -41
View File
@@ -1740,17 +1740,15 @@ processChatCommand' vr = \case
pure conn'
APIConnectPlan userId cLink -> withUserId userId $ \user ->
uncurry (CRConnectionPlan user) <$> connectPlan user cLink
APIPrepareContact userId link contactSLinkData -> withUserId userId $ \user -> do
APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do
let ContactShortLinkData {profile, welcomeMsg} = contactSLinkData
ct <- withStore $ \db -> createPreparedContact db user profile link
ct <- withStore $ \db -> createPreparedContact db user profile accLink
forM_ welcomeMsg $ \msg ->
createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent $ MCText msg) Nothing
pure $ CRNewPreparedContact user ct
APIPrepareGroup userId link groupSLinkData -> withUserId userId $ \user -> do
APIPrepareGroup userId accLink groupSLinkData -> withUserId userId $ \user -> do
let GroupShortLinkData {groupProfile} = groupSLinkData
-- TODO [short link] create host member for group connection on CONF, XGrpLinkInv (as in createGroupViaLink')
-- TODO - see other problems in createPreparedGroup: invited member id (user member), business chats
gInfo <- withStore $ \db -> createPreparedGroup db vr user groupProfile link
gInfo <- withStore $ \db -> createPreparedGroup db vr user groupProfile accLink
pure $ CRNewPreparedGroup user gInfo
-- TODO [short links] change prepared entity user
-- TODO - UI would call these APIs before APIConnectPrepared... APIs
@@ -1759,42 +1757,44 @@ processChatCommand' vr = \case
ok_
APIChangeGroupUser groupId newUserId -> withUser $ \user -> do
ok_
-- Alternative to passing incognito to APIConnectPreparedContact, APIConnectPreparedGroup would be to
-- create new APIs to set incognito on entity - APISetContactIncognito, APISetGroupIncognito.
-- It would be more complex:
-- - would require to persist incognito profile on entity opposing to connection as currently,
-- - would require decomposing part of APIConnect.
-- As it's an edge case / not a big issue that it's not persisted like a change of user,
-- we're simply passing it to prepare here.
APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user@User {userId} -> do
ct@Contact {connLinkToConnect} <- withFastStore $ \db -> getContact db vr user contactId
APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user -> do
Contact {connLinkToConnect} <- withFastStore $ \db -> getContact db vr user contactId
case connLinkToConnect of
Nothing -> throwCmdError "contact doesn't have link to connect"
Just link -> case link of
(ACCL SCMInvitation ccLink) ->
connectViaInvitation user incognito ccLink (Just contactId) >>= \case
CRSentConfirmation {} -> do
-- get updated contact with connection
ct' <- withFastStore $ \db -> getContact db vr user contactId
forM_ msgContent_ $ \mc -> do
let evt = XMsgNew $ MCSimple (extMsgContent mc Nothing)
(msg, _) <- sendDirectContactMessage user ct' evt
ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci]
pure $ CRStartedConnectionToContact user ct'
cr -> pure cr
(ACCL SCMContact ccLink) ->
connectViaContact user incognito ccLink msgContent_ (Just $ CGMContactId contactId) >>= \case
CRSentInvitation {} -> do
-- get updated contact with connection
ct' <- withFastStore $ \db -> getContact db vr user contactId
forM_ msgContent_ $ \mc ->
createInternalChatItem user (CDDirectSnd ct') (CISndMsgContent mc) Nothing
pure $ CRStartedConnectionToContact user ct'
cr -> pure cr
-- TODO [short links] connect to prepared group
Just (ACCL SCMInvitation ccLink) ->
connectViaInvitation user incognito ccLink (Just contactId) >>= \case
CRSentConfirmation {} -> do
-- get updated contact with connection
ct' <- withFastStore $ \db -> getContact db vr user contactId
forM_ msgContent_ $ \mc -> do
let evt = XMsgNew $ MCSimple (extMsgContent mc Nothing)
(msg, _) <- sendDirectContactMessage user ct' evt
ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci]
pure $ CRStartedConnectionToContact user ct'
cr -> pure cr
Just (ACCL SCMContact ccLink) ->
connectViaContact user incognito ccLink msgContent_ (Just $ CGMContactId contactId) >>= \case
CRSentInvitation {} -> do
-- get updated contact with connection
ct' <- withFastStore $ \db -> getContact db vr user contactId
forM_ msgContent_ $ \mc ->
createInternalChatItem user (CDDirectSnd ct') (CISndMsgContent mc) Nothing
pure $ CRStartedConnectionToContact user ct'
cr -> pure cr
APIConnectPreparedGroup groupId incognito -> withUser $ \user -> do
ok_
(gInfo, hostMember) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getHostMember db vr user groupId
let GroupInfo {connLinkToConnect} = gInfo
case connLinkToConnect of
Nothing -> throwCmdError "group doesn't have link to connect"
Just ccLink ->
connectViaContact user incognito ccLink Nothing (Just $ CGMGroupMemberId (groupMemberId' hostMember)) >>= \case
CRSentInvitation {connection = PendingContactConnection {pccConnId}} -> do
gInfo' <- withStore' $ \db -> do
setViaGroupLinkHash db groupId pccConnId
setGroupConnLinkStartedConnection db gInfo True
pure $ CRStartedConnectionToGroup user gInfo'
cr -> pure cr
APIConnect userId incognito (Just (ACCL SCMInvitation ccLink)) mc_ -> withUserId userId $ \user -> do
when (isJust mc_) $ throwChatError CEConnReqMessageProhibited
connectViaInvitation user incognito ccLink Nothing
@@ -2910,8 +2910,6 @@ processChatCommand' vr = \case
( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e
)
-- TODO [short links] Maybe Int64 should be Maybe <entity id type> to differentiate between contact and group links;
-- TODO link connection to entity in createConnReqConnection
connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> Maybe MsgContent -> Maybe ContactOrGroupMemberId -> CM ChatResponse
connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) mc_ comId_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
+16 -1
View File
@@ -677,7 +677,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> pure ()
processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM ()
processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of
processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, customUserProfileId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of
INV (ACR _ cReq) ->
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
case cReq of
@@ -735,6 +735,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
allowAgentConnectionAsync user conn' confId XOk
| otherwise -> messageError "x.grp.acpt: memberId is different from expected"
_ -> messageError "CONF from invited member must have x.grp.acpt"
GCHostMember ->
case chatMsgEvent of
XGrpLinkInv glInv -> do
-- XGrpLinkInv here means we are connecting via prepared group, and we have to update user and host member records
(gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db vr user gInfo m glInv
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId)
let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing True
allowAgentConnectionAsync user conn' confId $ XInfo profileToSend
toView $ CEvtGroupLinkConnecting user gInfo' m'
XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do
(gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersRejected db vr user gInfo m glRjct
toView $ CEvtGroupLinkConnecting user gInfo' m'
toViewTE $ TEGroupLinkRejected user gInfo' rejectionReason
_ -> messageError "CONF from host member in prepared group must have x.grp.link.inv or x.grp.link.reject"
_ ->
case chatMsgEvent of
XGrpMemInfo memId _memProfile
+2 -1
View File
@@ -139,7 +139,8 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
+5 -5
View File
@@ -169,7 +169,7 @@ createConnReqConnection db userId acId cReqHash sLnk comId_ xContactId incognito
created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, acId, pccConnStatus, ConnContact, BI True)
( (userId, acId, pccConnStatus, connType, BI True)
:. (cReqHash, sLnk, contactId_, groupMemberId_)
:. (xContactId, customUserProfileId, BI (isJust groupLinkId), groupLinkId)
:. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup)
@@ -177,10 +177,10 @@ createConnReqConnection db userId acId cReqHash sLnk comId_ xContactId incognito
pccConnId <- insertedRowId db
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connLinkInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
where
(contactId_, groupMemberId_) = case comId_ of
Just (CGMContactId ctId) -> (Just ctId, Nothing)
Just (CGMGroupMemberId gmId) -> (Nothing, Just gmId)
Nothing -> (Nothing, Nothing)
(connType, contactId_, groupMemberId_) = case comId_ of
Just (CGMContactId ctId) -> (ConnContact, Just ctId, Nothing)
Just (CGMGroupMemberId gmId) -> (ConnMember, Nothing, Just gmId)
Nothing -> (ConnContact, Nothing, Nothing)
getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId)
getConnReqContactXContactId db vr user@User {userId} cReqHash = do
+119 -20
View File
@@ -33,6 +33,9 @@ module Simplex.Chat.Store.Groups
createGroupInvitation,
deleteContactCardKeepConn,
createPreparedGroup,
setGroupConnLinkStartedConnection,
updatePreparedUserAndHostMembersInvited,
updatePreparedUserAndHostMembersRejected,
createGroupInvitedViaLink,
createGroupRejectedViaLink,
setViaGroupLinkHash,
@@ -50,6 +53,7 @@ module Simplex.Chat.Store.Groups
getActiveMembersByName,
getGroupInfoByName,
getGroupMember,
getHostMember,
getMentionedGroupMember,
getMentionedMemberByMemberId,
getGroupMemberById,
@@ -156,6 +160,7 @@ import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
import Data.Ord (Down (..))
import Data.Text (Text)
import Data.Time.Clock (UTCTime (..), getCurrentTime)
import Data.Text.Encoding (encodeUtf8)
import Simplex.Chat.Messages
import Simplex.Chat.Protocol (MsgMention (..), groupForwardVersion)
import Simplex.Chat.Store.Direct
@@ -284,7 +289,8 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = do
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
@@ -367,6 +373,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs,
connLinkToConnect = Nothing,
connLinkStartedConnection = False,
chatTags = [],
chatItemTTL = Nothing,
uiThemes = Nothing,
@@ -439,6 +446,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs,
connLinkToConnect = Nothing,
connLinkStartedConnection = False,
chatTags = [],
chatItemTTL = Nothing,
uiThemes = Nothing,
@@ -537,22 +545,102 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile {
DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId)
createPreparedGroup :: DB.Connection -> VersionRangeChat -> User -> GroupProfile -> ACreatedConnLink -> ExceptT StoreError IO GroupInfo
createPreparedGroup db vr user@User {userId} groupProfile connLinkToConnect = do
createPreparedGroup db vr user@User {userId, userContactId} groupProfile connLinkToConnect = do
currentTs <- liftIO getCurrentTime
-- TODO [short links] support preparing business chats
let business = Nothing
groupId <- createGroup_ db userId groupProfile (Just connLinkToConnect) business currentTs
-- TODO [short links] create "unknown" host member here? set invitedByGroupMemberId later?
-- TODO - same for invitedMember
-- TODO - for membershipStatus - new status GSMemNew?
-- TODO - customUserProfileId - pass on APIConnectPreparedGroup, update member; or separate apis for switching before joining?
let invitedByGroupMemberId = Nothing
invitedMember = MemberIdRole (MemberId "unknown") GRMember
membershipStatus = GSMemAccepted
customUserProfileId = Nothing
void $ createContactMemberInv_ db user groupId invitedByGroupMemberId user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId currentTs vr
-- TODO [short links] review: setViaGroupLinkHash
(groupId, groupLDN) <- createGroup_ db userId groupProfile (Just connLinkToConnect) Nothing currentTs
hostMemberId <- insertHost_ currentTs groupId groupLDN
let userMember = MemberIdRole (MemberId $ encodeUtf8 groupLDN <> "_user_unknown_id") GRMember
void $ createContactMemberInv_ db user groupId (Just hostMemberId) user userMember GCUserMember GSMemUnknown IBUnknown Nothing currentTs vr
getGroupInfo db vr user groupId
where
insertHost_ currentTs groupId groupLDN = do
let memberId = MemberId $ encodeUtf8 groupLDN <> "_host_unknown_id"
hostProfile = profileFromName $ nameFromMemberId memberId
(localDisplayName, profileId) <- createNewMemberProfile_ db user hostProfile currentTs
liftIO $ do
DB.execute
db
[sql|
INSERT INTO group_members
( group_id, member_id, member_role, member_category, member_status, invited_by,
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (groupId, memberId, GRAdmin, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
)
insertedRowId db
setGroupConnLinkStartedConnection :: DB.Connection -> GroupInfo -> Bool -> IO GroupInfo
setGroupConnLinkStartedConnection db groupInfo@GroupInfo {groupId} connLinkStartedConnection = do
currentTs <- getCurrentTime
DB.execute
db
"UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE group_id = ?"
(BI connLinkStartedConnection, currentTs, groupId)
pure groupInfo {connLinkStartedConnection = connLinkStartedConnection}
updatePreparedUserAndHostMembersInvited :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember)
updatePreparedUserAndHostMembersInvited db vr user gInfo hostMember GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted} = do
let fromMemberProfile = profileFromName fromMemberName
initialStatus = maybe GSMemAccepted (acceptanceToStatus $ memberAdmission groupProfile) accepted
updatePreparedUserAndHostMembers' db vr user gInfo hostMember fromMember fromMemberProfile invitedMember groupProfile initialStatus
updatePreparedUserAndHostMembersRejected :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember)
updatePreparedUserAndHostMembersRejected db vr user gInfo hostMember GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do
let fromMemberProfile = profileFromName $ nameFromMemberId memberId
updatePreparedUserAndHostMembers' db vr user gInfo hostMember fromMember fromMemberProfile invitedMember groupProfile GSMemRejected
updatePreparedUserAndHostMembers' :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember)
updatePreparedUserAndHostMembers'
db
vr
user
gInfo@GroupInfo {groupId, groupProfile = gp}
hostMember
fromMember
fromMemberProfile
invitedMember
groupProfile
membershipStatus = do
currentTs <- liftIO getCurrentTime
liftIO $ updateUserMember currentTs
hostMember' <- updateHostMember currentTs
when (gp /= groupProfile) $
void $ updateGroupProfile db user gInfo groupProfile
gInfo' <- getGroupInfo db vr user groupId
pure (gInfo', hostMember')
where
updateUserMember currentTs = do
let GroupInfo {membership} = gInfo
MemberIdRole memberId memberRole = invitedMember
DB.execute
db
[sql|
UPDATE group_members
SET member_id = ?,
member_role = ?,
member_status = ?,
updated_at = ?
WHERE group_member_id = ?
|]
(memberId, memberRole, membershipStatus, currentTs, groupMemberId' membership)
updateHostMember currentTs = do
_ <- updateMemberProfile db user hostMember fromMemberProfile
let MemberIdRole memberId memberRole = fromMember
gmId = groupMemberId' hostMember
liftIO $
DB.execute
db
[sql|
UPDATE group_members
SET member_id = ?,
member_role = ?,
updated_at = ?
WHERE group_member_id = ?
|]
(memberId, memberRole, currentTs, gmId)
getGroupMemberById db vr user gmId
createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember)
createGroupInvitedViaLink db vr user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do
@@ -578,7 +666,7 @@ createGroupViaLink'
business
membershipStatus = do
currentTs <- liftIO getCurrentTime
groupId <- createGroup_ db userId groupProfile Nothing business currentTs
(groupId, _groupLDN) <- createGroup_ db userId groupProfile Nothing business currentTs
hostMemberId <- insertHost_ currentTs groupId
liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId)
-- using IBUnknown since host is created without contact
@@ -603,7 +691,7 @@ createGroupViaLink'
)
insertedRowId db
createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe ACreatedConnLink -> Maybe BusinessChatInfo -> UTCTime -> ExceptT StoreError IO GroupId
createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe ACreatedConnLink -> Maybe BusinessChatInfo -> UTCTime -> ExceptT StoreError IO (GroupId, Text)
createGroup_ db userId groupProfile connLinkToConnect business currentTs = ExceptT $ do
let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
@@ -623,7 +711,8 @@ createGroup_ db userId groupProfile connLinkToConnect business currentTs = Excep
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. connLinkToConnectRow connLinkToConnect :. businessChatInfoRow business)
insertedRowId db
groupId <- insertedRowId db
pure (groupId, localDisplayName)
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
setViaGroupLinkHash db groupId connId =
@@ -800,7 +889,8 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do
SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
@@ -893,6 +983,14 @@ getGroupMember db vr user@User {userId} groupId groupMemberId =
(groupMemberQuery <> " WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ?")
(userId, groupId, groupMemberId, userId)
getHostMember :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO GroupMember
getHostMember db vr user@User {userId} groupId =
ExceptT . firstRow (toContactMember vr user) (SEGroupHostMemberNotFound groupId) $
DB.query
db
(groupMemberQuery <> " WHERE m.group_id = ? AND m.member_category = ?")
(userId, groupId, GCHostMember)
getMentionedGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO CIMention
getMentionedGroupMember db User {userId} groupId gmId =
ExceptT $
@@ -1660,7 +1758,8 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
@@ -13,8 +13,10 @@ m20250526_short_links =
[sql|
ALTER TABLE contacts ADD COLUMN conn_full_link_to_connect BLOB;
ALTER TABLE contacts ADD COLUMN conn_short_link_to_connect BLOB;
ALTER TABLE groups ADD COLUMN conn_full_link_to_connect BLOB;
ALTER TABLE groups ADD COLUMN conn_short_link_to_connect BLOB;
ALTER TABLE groups ADD COLUMN conn_link_started_connection INTEGER NOT NULL DEFAULT 0;
|]
down_m20250526_short_links :: Query
@@ -22,6 +24,8 @@ down_m20250526_short_links =
[sql|
ALTER TABLE contacts DROP COLUMN conn_full_link_to_connect;
ALTER TABLE contacts DROP COLUMN conn_short_link_to_connect;
ALTER TABLE groups DROP COLUMN conn_full_link_to_connect;
ALTER TABLE groups DROP COLUMN conn_short_link_to_connect;
ALTER TABLE groups DROP COLUMN conn_link_started_connection;
|]
@@ -46,7 +46,8 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
@@ -277,6 +278,16 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
LIST SUBQUERY 1
SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE group_members
SET member_id = ?,
member_role = ?,
updated_at = ?
WHERE group_member_id = ?
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE xftp_file_descriptions
SET file_descr_text = ?, file_descr_part_no = ?, file_descr_complete = ?
@@ -294,6 +305,14 @@ Query:
Plan:
SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?)
Query:
INSERT INTO group_members
( group_id, member_id, member_role, member_category, member_status, invited_by,
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
Query:
INSERT INTO group_members
( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
@@ -656,6 +675,17 @@ Query:
Plan:
SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE group_members
SET member_id = ?,
member_role = ?,
member_status = ?,
updated_at = ?
WHERE group_member_id = ?
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query:
DELETE FROM chat_item_reactions
WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? AND reaction = ?
@@ -853,7 +883,8 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
@@ -902,7 +933,8 @@ Query:
SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
@@ -4580,7 +4612,8 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupMember - membership
@@ -4605,7 +4638,8 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupMember - membership
@@ -4683,6 +4717,31 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?)
Query:
SELECT
m.group_member_id, m.group_id, 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.image, p.contact_link, p.local_alias, p.preferences,
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,
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.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.snd_file_id, c.rcv_file_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
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.connection_id = (
SELECT max(cc.connection_id)
FROM connections cc
WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id
)
WHERE m.group_id = ? AND m.member_category = ?
Plan:
SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=?)
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1
SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?)
Query:
SELECT
m.group_member_id, m.group_id, 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,
@@ -6013,6 +6072,10 @@ Query: UPDATE groups SET chat_ts = ? WHERE user_id = ? AND group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
@@ -139,7 +139,8 @@ CREATE TABLE groups(
local_alias TEXT DEFAULT '',
members_require_attention INTEGER NOT NULL DEFAULT 0,
conn_full_link_to_connect BLOB,
conn_short_link_to_connect BLOB, -- received
conn_short_link_to_connect BLOB,
conn_link_started_connection INTEGER NOT NULL DEFAULT 0, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
+6 -4
View File
@@ -89,6 +89,7 @@ data StoreError
| SEGroupNotFoundByName {groupName :: GroupName}
| SEGroupMemberNameNotFound {groupId :: GroupId, groupMemberName :: ContactName}
| SEGroupMemberNotFound {groupMemberId :: GroupMemberId}
| SEGroupHostMemberNotFound {groupId :: GroupId}
| SEGroupMemberNotFoundByMemberId {memberId :: MemberId}
| SEMemberContactGroupMemberNotFound {contactId :: ContactId}
| SEGroupWithoutUser
@@ -597,12 +598,12 @@ safeDeleteLDN db User {userId} localDisplayName = do
type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId)
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe ConnReqContact, Maybe ShortLinkContact) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int) :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int) :. GroupMemberRow
type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime)
toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, connFullLink, connShortLink) :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention) :. userMemberRow) =
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. (connFullLink, connShortLink, BI connLinkStartedConnection) :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
@@ -611,7 +612,7 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName,
connLinkToConnect = case (connFullLink, connShortLink) of
(Nothing, _) -> Nothing
(Just fullLink, shortLink_) -> Just $ CCLink fullLink shortLink_
in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, connLinkToConnect, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention}
in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, connLinkToConnect, connLinkStartedConnection, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention}
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences) :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs)) =
@@ -644,7 +645,8 @@ groupInfoQuery =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupMember - membership
+1
View File
@@ -420,6 +420,7 @@ data GroupInfo = GroupInfo
chatTs :: Maybe UTCTime,
userMemberProfileSentAt :: Maybe UTCTime,
connLinkToConnect :: Maybe CreatedLinkContact,
connLinkStartedConnection :: Bool,
chatTags :: [ChatTagId],
chatItemTTL :: Maybe Int64,
uiThemes :: Maybe UIThemeEntityOverrides,
+1
View File
@@ -195,6 +195,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"]
CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
CRStartedConnectionToContact u c -> ttyUser u [ttyContact' c <> ": connection started"]
CRStartedConnectionToGroup u g -> ttyUser u [ttyGroup' g <> ": connection started"]
CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
CRItemsReadForChat u _chatId -> ttyUser u ["items read for chat"]
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
+65 -2
View File
@@ -17,11 +17,11 @@ import Control.Monad.Except
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks)
import Simplex.Chat.Options
import Simplex.Chat.Protocol (currentChatVersion)
import Simplex.Chat.Store.Shared (createContact)
import Simplex.Chat.Types (ConnStatus (..), Profile (..))
import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Env.SQLite
@@ -109,6 +109,8 @@ chatProfileTests = do
describe "connection via prepared entity" $ do
it "prepare contact using invitation short link data and connect" testShortLinkInvitationPrepareContact
it "prepare contact using address short link data and connect" testShortLinkAddressPrepareContact
it "prepare group using group short link data and connect" testShortLinkPrepareGroup
it "prepare group using group short link data and connect, host rejects" testShortLinkPrepareGroupReject
testUpdateProfile :: HasCallStack => TestParams -> IO ()
testUpdateProfile =
@@ -2810,3 +2812,64 @@ testShortLinkAddressPrepareContact =
(bob <## "alice (Alice): contact is connected")
(alice <## "bob (Bob): contact is connected")
alice <##> bob
testShortLinkPrepareGroup :: HasCallStack => TestParams -> IO ()
testShortLinkPrepareGroup =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice cath
alice ##> "/create link #team short"
(shortLink, fullLink) <- getShortGroupLink alice "team" GRMember True
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "group link: ok to connect"
groupSLinkData <- getTermLine bob
bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData)
bob <## "#team: group is prepared"
bob ##> "/_connect group #1"
bob <## "#team: connection started"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice <## "#team: bob joined the group",
do
bob <## "#team: joining the group..."
bob <## "#team: you joined the group"
bob <## "#team: member cath (Catherine) is connected",
do
cath <## "#team: alice added bob (Bob) to the group (connecting...)"
cath <## "#team: new member bob is connected"
]
alice #> "#team 1"
[bob, cath] *<# "#team alice> 1"
bob #> "#team 2"
[alice, cath] *<# "#team bob> 2"
cath #> "#team 3"
[alice, bob] *<# "#team cath> 3"
testShortLinkPrepareGroupReject :: HasCallStack => TestParams -> IO ()
testShortLinkPrepareGroupReject =
testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice cath
alice ##> "/create link #team short"
(shortLink, fullLink) <- getShortGroupLink alice "team" GRMember True
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "group link: ok to connect"
groupSLinkData <- getTermLine bob
bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData)
bob <## "#team: group is prepared"
bob ##> "/_connect group #1"
bob <## "#team: connection started"
alice <## "bob (Bob): rejecting request to join group #team, reason: GRRBlockedName"
bob <## "#team: joining the group..."
bob <## "#team: join rejected, reason: GRRBlockedName"
alice #> "#team 1"
cath <# "#team alice> 1"
cath #> "#team 2"
alice <# "#team cath> 2"
-- rejected member can't send messages to group
bob ##> "#team hello"
bob <## "bad chat command: not current member"
where
cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Left GRRBlockedName)}}