mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-09 18:51:45 +00:00
core: prepare and connect to group (#5964)
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)}}
|
||||
|
||||
Reference in New Issue
Block a user