diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 2ddaf1d2af..9d359d9aa6 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -80,6 +80,7 @@ enum ChatCommand: ChatCmdProtocol { case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) case apiDeleteGroupLink(groupId: Int64) case apiGetGroupLink(groupId: Int64) + case apiAddShortLinkGroupLink(groupId: Int64) case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) case apiTestProtoServer(userId: Int64, server: String) @@ -134,6 +135,7 @@ enum ChatCommand: ChatCmdProtocol { case apiCreateMyAddress(userId: Int64, short: Bool) case apiDeleteMyAddress(userId: Int64) case apiShowMyAddress(userId: Int64) + case apiAddShortLinkMyAddress(userId: Int64) case apiSetProfileAddress(userId: Int64, on: Bool) case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?) case apiAcceptContact(incognito: Bool, contactReqId: Int64) @@ -262,6 +264,7 @@ enum ChatCommand: ChatCmdProtocol { case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)" case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)" case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" + case let .apiAddShortLinkGroupLink(groupId): return "/_short link #\(groupId)" case let .apiCreateMemberContact(groupId, groupMemberId): return "/_create member contact #\(groupId) \(groupMemberId)" case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)" case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)" @@ -326,6 +329,7 @@ enum ChatCommand: ChatCmdProtocol { case let .apiCreateMyAddress(userId, short): return "/_address \(userId) short=\(onOff(short))" case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" case let .apiShowMyAddress(userId): return "/_show_address \(userId)" + case let .apiAddShortLinkMyAddress(userId): return "/_short_link_address \(userId)" case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))" case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)" @@ -438,6 +442,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" case .apiDeleteGroupLink: return "apiDeleteGroupLink" case .apiGetGroupLink: return "apiGetGroupLink" + case .apiAddShortLinkGroupLink: return "apiAddShortLinkGroupLink" case .apiCreateMemberContact: return "apiCreateMemberContact" case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" case .apiTestProtoServer: return "apiTestProtoServer" @@ -491,6 +496,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiCreateMyAddress: return "apiCreateMyAddress" case .apiDeleteMyAddress: return "apiDeleteMyAddress" case .apiShowMyAddress: return "apiShowMyAddress" + case .apiAddShortLinkMyAddress: return "apiAddShortLinkMyAddress" case .apiSetProfileAddress: return "apiSetProfileAddress" case .apiAddressAutoAccept: return "apiAddressAutoAccept" case .apiAcceptContact: return "apiAcceptContact" diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6b938aaa4d..952702b8a8 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1210,6 +1210,13 @@ private func userAddressResponse(_ r: APIResult) throws -> UserCo } } +func apiAddShortLinkMyAddress() async throws -> UserContactLink { + let userId = try currentUserId("apiAddShortLinkMyAddress") + let r: ChatResponse1 = try await chatSendCmd(.apiAddShortLinkMyAddress(userId: userId)) + if case let .userContactLink(_, contactLink) = r { return contactLink } + throw r.unexpected +} + func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? { let userId = try currentUserId("userAddressAutoAccept") let r: APIResult = await chatApiSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept)) @@ -1736,6 +1743,12 @@ func apiGetGroupLink(_ groupId: Int64) throws -> (CreatedConnLink, GroupMemberRo } } +func apiAddShortLinkGroupLink(_ groupId: Int64) async throws -> (CreatedConnLink, GroupMemberRole) { + let r: ChatResponse2 = try await chatSendCmd(.apiAddShortLinkGroupLink(groupId: groupId)) + if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) } + throw r.unexpected +} + func apiCreateMemberContact(_ groupId: Int64, _ groupMemberId: Int64) async throws -> Contact { let r: ChatResponse2 = try await chatSendCmd(.apiCreateMemberContact(groupId: groupId, groupMemberId: groupMemberId)) if case let .newMemberContact(_, contact, _, _) = r { return contact } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index a11c073a42..bfc7c19fa7 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -35,16 +35,23 @@ struct GroupLinkView: View { } var body: some View { - if creatingGroup { - groupLinkView() - .navigationBarBackButtonHidden() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button ("Continue") { linkCreatedCb?() } + ZStack { + if creatingGroup { + groupLinkView() + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button ("Continue") { linkCreatedCb?() } + } } - } - } else { - groupLinkView() + } else { + groupLinkView() + } + if creatingLink { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity) + } } } @@ -79,6 +86,14 @@ struct GroupLinkView: View { Label("Share link", systemImage: "square.and.arrow.up") } + if (groupLink.connShortLink == nil && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)) { + Button { + addShortLink() + } label: { + Label("Add short link", systemImage: "plus") + } + } + if !creatingGroup { Button(role: .destructive) { alert = .deleteLink } label: { Label("Delete link", systemImage: "trash") @@ -89,11 +104,6 @@ struct GroupLinkView: View { Label("Create link", systemImage: "link.badge.plus") } .disabled(creatingLink) - if creatingLink { - ProgressView() - .scaleEffect(2) - .frame(maxWidth: .infinity) - } } } header: { if let groupLink, groupLink.connShortLink != nil { @@ -160,6 +170,26 @@ struct GroupLinkView: View { } } } + + private func addShortLink() { + Task { + do { + creatingLink = true + let link = try await apiAddShortLinkGroupLink(groupId) + await MainActor.run { + creatingLink = false + (groupLink, groupLinkMemberRole) = link + } + } catch let error { + logger.error("apiAddShortLinkGroupLink: \(responseError(error))") + await MainActor.run { + creatingLink = false + let a = getErrorAlert(error, "Error adding short link") + alert = .error(title: a.title, error: a.message) + } + } + } + } } struct GroupLinkView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 4813edf96c..1140dcf0fc 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -153,6 +153,9 @@ struct UserAddressView: View { } } addressSettingsButton(userAddress) + if (userAddress.connLinkContact.connShortLink == nil && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)) { + addShortLinkButton() + } } header: { ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink) } footer: { @@ -209,6 +212,30 @@ struct UserAddressView: View { } } + private func addShortLinkButton() -> some View { + Button { + addShortLink() + } label: { + Label("Add short link", systemImage: "plus") + } + } + + private func addShortLink() { + progressIndicator = true + Task { + do { + let userAddress = try await apiAddShortLinkMyAddress() + await MainActor.run { + chatModel.userAddress = userAddress + } + await MainActor.run { progressIndicator = false } + } catch let error { + logger.error("apiAddShortLinkMyAddress: \(responseError(error))") + await MainActor.run { progressIndicator = false } + } + } + } + private func createOneTimeLinkButton() -> some View { NavigationLink { NewChatView(selection: .invite) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 7cb2d9fe5e..a334bb9a06 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1570,6 +1570,14 @@ object ChatController { return null } + suspend fun apiAddShortLinkMyAddress(rh: Long?): UserContactLinkRec? { + val userId = kotlin.runCatching { currentUserId("apiAddShortLinkMyAddress") }.getOrElse { return null } + val r = sendCmd(rh, CC.ApiAddShortLinkMyAddress(userId)) + if (r is API.Result && r.res is CR.UserContactLink) return r.res.contactLink + Log.e(TAG, "apiAddShortLinkMyAddress bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun userAddressAutoAccept(rh: Long?, autoAccept: AutoAccept?): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("userAddressAutoAccept") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiAddressAutoAccept(userId, autoAccept)) @@ -2016,6 +2024,13 @@ object ChatController { return null } + suspend fun apiAddShortLinkGroupLink(rh: Long?, groupId: Long): Pair? { + val r = sendCmd(rh, CC.APIAddShortLinkGroupLink(groupId)) + if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + Log.e(TAG, "apiAddShortLinkGroupLink bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiCreateMemberContact(rh: Long?, groupId: Long, groupMemberId: Long): Contact? { val r = sendCmd(rh, CC.APICreateMemberContact(groupId, groupMemberId)) if (r is API.Result && r.res is CR.NewMemberContact) return r.res.contact @@ -3368,6 +3383,7 @@ sealed class CC { class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC() + class APIAddShortLinkGroupLink(val groupId: Long): CC() class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC() class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC() class APITestProtoServer(val userId: Long, val server: String): CC() @@ -3422,6 +3438,7 @@ sealed class CC { class ApiCreateMyAddress(val userId: Long, val short: Boolean): CC() class ApiDeleteMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC() + class ApiAddShortLinkMyAddress(val userId: Long): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC() class ApiGetCallInvitations: CC() @@ -3555,6 +3572,7 @@ sealed class CC { is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIDeleteGroupLink -> "/_delete link #$groupId" is APIGetGroupLink -> "/_get link #$groupId" + is APIAddShortLinkGroupLink -> "/_short link #$groupId" is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId" is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}" is APITestProtoServer -> "/_server test $userId $server" @@ -3609,6 +3627,7 @@ sealed class CC { is ApiCreateMyAddress -> "/_address $userId short=${onOff(short)}" is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiShowMyAddress -> "/_show_address $userId" + is ApiAddShortLinkMyAddress -> "/_short_link_address $userId" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}" is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId" @@ -3720,6 +3739,7 @@ sealed class CC { is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole" is APIDeleteGroupLink -> "apiDeleteGroupLink" is APIGetGroupLink -> "apiGetGroupLink" + is APIAddShortLinkGroupLink -> "apiAddShortLinkGroupLink" is APICreateMemberContact -> "apiCreateMemberContact" is APISendMemberContactInvitation -> "apiSendMemberContactInvitation" is APITestProtoServer -> "testProtoServer" @@ -3774,6 +3794,7 @@ sealed class CC { is ApiCreateMyAddress -> "apiCreateMyAddress" is ApiDeleteMyAddress -> "apiDeleteMyAddress" is ApiShowMyAddress -> "apiShowMyAddress" + is ApiAddShortLinkMyAddress -> "apiAddShortLinkMyAddress" is ApiSetProfileAddress -> "apiSetProfileAddress" is ApiAddressAutoAccept -> "apiAddressAutoAccept" is ApiAcceptContact -> "apiAcceptContact" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 6e1b9a731d..7e3fd839e2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -49,6 +49,18 @@ fun GroupLinkView( creatingLink = false } } + fun addShortLink() { + creatingLink = true + withBGApi { + val link = chatModel.controller.apiAddShortLinkGroupLink(rhId, groupInfo.groupId) + if (link != null) { + groupLink = link.first + groupLinkMemberRole.value = link.second + onGroupLinkUpdated?.invoke(link) + } + creatingLink = false + } + } LaunchedEffect(Unit) { if (groupLink == null && !creatingLink) { createLink() @@ -60,6 +72,7 @@ fun GroupLinkView( groupLinkMemberRole, creatingLink, createLink = ::createLink, + addShortLink = ::addShortLink, updateLink = { val role = groupLinkMemberRole.value if (role != null) { @@ -105,6 +118,7 @@ fun GroupLinkLayout( groupLinkMemberRole: MutableState, creatingLink: Boolean, createLink: () -> Unit, + addShortLink: () -> Unit, updateLink: () -> Unit, deleteLink: () -> Unit, creatingGroup: Boolean = false, @@ -182,12 +196,26 @@ fun GroupLinkLayout( ) } } + if (groupLink.connShortLink == null && appPreferences.privacyShortLinks.get()) { + AddShortLinkButton(addShortLink) + } } } SectionBottomSpacer() } } +@Composable +private fun AddShortLinkButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.add_short_link), + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState, enabled: Boolean = true) { Row( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 8c7c2d8416..53237f48ac 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -43,6 +43,7 @@ fun UserAddressView( KeyChangeEffect(user.value?.remoteHostId, user.value?.userId) { close() } + fun setProfileAddress(on: Boolean) { progressIndicator = true withBGApi { @@ -81,6 +82,17 @@ fun UserAddressView( } } + fun addShortLink() { + withBGApi { + progressIndicator = true + val userAddress = chatModel.controller.apiAddShortLinkMyAddress(user.value?.remoteHostId) + if (userAddress != null) { + chatModel.userAddress.value = userAddress + } + progressIndicator = false + } + } + LaunchedEffect(autoCreateAddress) { if (chatModel.userAddress.value == null && autoCreateAddress) { createAddress() @@ -95,6 +107,7 @@ fun UserAddressView( userAddress = userAddress.value, shareViaProfile, createAddress = { createAddress() }, + addShortLink = { addShortLink() }, learnMore = { ModalManager.start.showModal { UserAddressLearnMore() @@ -169,6 +182,7 @@ private fun UserAddressLayout( userAddress: UserContactLinkRec?, shareViaProfile: MutableState, createAddress: () -> Unit, + addShortLink: () -> Unit, learnMore: () -> Unit, share: (String) -> Unit, sendEmail: (UserContactLinkRec) -> Unit, @@ -211,6 +225,9 @@ private fun UserAddressLayout( // ShareViaEmailButton { sendEmail(userAddress) } BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) + if (userAddress.connLinkContact.connShortLink == null && appPreferences.privacyShortLinks.get()) { + AddShortLinkButton(addShortLink) + } if (autoAcceptState.value.business) { SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) @@ -248,6 +265,17 @@ private fun CreateAddressButton(onClick: () -> Unit) { ) } +@Composable +private fun AddShortLinkButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.add_short_link), + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun CreateOneTimeLinkButton() { val closeAll = { ModalManager.start.closeModals() } @@ -559,6 +587,7 @@ fun PreviewUserAddressLayoutNoAddress() { user = User.sampleData, userAddress = null, createAddress = {}, + addShortLink = {}, share = { _ -> }, deleteAddress = {}, saveAas = { _, _ -> }, @@ -592,6 +621,7 @@ fun PreviewUserAddressLayoutAddressCreated() { user = User.sampleData, userAddress = UserContactLinkRec(CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null)), createAddress = {}, + addShortLink = {}, share = { _ -> }, deleteAddress = {}, saveAas = { _, _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index b5bf2efaff..7af75e64e8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1058,6 +1058,7 @@ Address settings Business address Add your team members to the conversations. + Add short link Continue diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 061fbfc8d6..5e15c6fb7a 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -373,6 +373,7 @@ data ChatCommand | APIGroupLinkMemberRole GroupId GroupMemberRole | APIDeleteGroupLink GroupId | APIGetGroupLink GroupId + | APIAddShortLinkGroupLink GroupId | APICreateMemberContact GroupId GroupMemberId | APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent} | GetUserProtoServers AProtocolType @@ -461,6 +462,7 @@ data ChatCommand | DeleteMyAddress | APIShowMyAddress UserId | ShowMyAddress + | APIAddShortLinkMyAddress UserId | APISetProfileAddress UserId Bool | SetProfileAddress Bool | APIAddressAutoAccept UserId (Maybe AutoAccept) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 20f9468cd6..a565d90696 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1796,9 +1796,9 @@ processChatCommand' vr = \case CreateMyAddress short -> withUser $ \User {userId} -> processChatCommand $ APICreateMyAddress userId short APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do - conns <- withFastStore $ \db -> getUserAddressConnections db vr user + conn <- withFastStore $ \db -> getUserAddressConnection db vr user withChatLock "deleteMyAddress" $ do - deleteAgentConnectionsAsync $ map aConnId conns + deleteAgentConnectionAsync $ aConnId conn withFastStore' (`deleteUserAddress` user) let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} r <- updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing @@ -1812,6 +1812,17 @@ processChatCommand' vr = \case CRUserContactLink user <$> withFastStore (`getUserAddress` user) ShowMyAddress -> withUser' $ \User {userId} -> processChatCommand $ APIShowMyAddress userId + APIAddShortLinkMyAddress userId -> withUserId' userId $ \user -> do + (ucl@UserContactLink {connLinkContact = CCLink connFullLink sLnk_}, conn) <- + withFastStore $ \db -> (,) <$> getUserAddress db user <*> getUserAddressConnection db vr user + when (isJust sLnk_) $ throwCmdError "address already has short link" + sLnk <- withAgent $ \a -> setContactShortLink a (aConnId conn) "" + case entityId conn of + Just uclId -> do + withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk + let ucl' = (ucl :: UserContactLink) {connLinkContact = CCLink connFullLink (Just sLnk)} + pure $ CRUserContactLink user ucl' + Nothing -> throwChatError $ CEException "no user contact link id" APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing @@ -2403,6 +2414,17 @@ processChatCommand' vr = \case gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId (_, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo pure $ CRGroupLink user gInfo groupLink mRole + APIAddShortLinkGroupLink groupId -> withUser $ \user -> do + (gInfo, (uclId, _gLink@(CCLink connFullLink sLnk_), mRole), conn) <- withFastStore $ \db -> do + gInfo <- getGroupInfo db vr user groupId + gLink <- getGroupLink db user gInfo + conn <- getGroupLinkConnection db vr user gInfo + pure (gInfo, gLink, conn) + when (isJust sLnk_) $ throwCmdError "group link already has short link" + sLnk <- withAgent $ \a -> setContactShortLink a (aConnId conn) "" + withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk + let groupLink' = CCLink connFullLink (Just sLnk) + pure $ CRGroupLink user gInfo groupLink' mRole APICreateMemberContact gId gMemberId -> withUser $ \user -> do (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId assertUserGroupRole g GRAuthor @@ -4222,6 +4244,7 @@ chatCommandP = "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), "/_get link #" *> (APIGetGroupLink <$> A.decimal), + "/_short link #" *> (APIAddShortLinkGroupLink <$> A.decimal), "/create link #" *> (CreateGroupLink <$> displayNameP <*> (memberRole <|> pure GRMember) <*> shortP), "/set link role #" *> (GroupLinkMemberRole <$> displayNameP <*> memberRole), "/delete link #" *> (DeleteGroupLink <$> displayNameP), @@ -4277,6 +4300,7 @@ chatCommandP = ("/delete_address" <|> "/da") $> DeleteMyAddress, "/_show_address " *> (APIShowMyAddress <$> A.decimal), ("/show_address" <|> "/sa") $> ShowMyAddress, + "/_short_link_address " *> (APIAddShortLinkMyAddress <$> A.decimal), "/_profile_address " *> (APISetProfileAddress <$> A.decimal <* A.space <*> onOffP), ("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP), "/_auto_accept " *> (APIAddressAutoAccept <$> A.decimal <* A.space <*> autoAcceptP), diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index a7dc154d9d..4986ed5140 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -43,7 +43,7 @@ module Simplex.Chat.Store.Profiles setUserProfileContactLink, getUserContactProfiles, createUserContactLink, - getUserAddressConnections, + getUserAddressConnection, getUserContactLinks, deleteUserAddress, getUserAddress, @@ -51,6 +51,7 @@ module Simplex.Chat.Store.Profiles getGroupLinkInfo, getUserContactLinkByConnReq, getUserContactLinkViaShortLink, + setUserContactLinkShortLink, getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, @@ -363,26 +364,21 @@ createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMo userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff -getUserAddressConnections :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO [Connection] -getUserAddressConnections db vr User {userId} = do - cs <- liftIO getUserAddressConnections_ - if null cs then throwError SEUserContactLinkNotFound else pure cs - where - getUserAddressConnections_ :: IO [Connection] - getUserAddressConnections_ = - map (toConnection vr) - <$> DB.query - db - [sql| - SELECT 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 connections c - JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id - WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL - |] - (userId, userId) +getUserAddressConnection :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO Connection +getUserAddressConnection db vr User {userId} = do + ExceptT . firstRow (toConnection vr) SEUserContactLinkNotFound $ + DB.query + db + [sql| + SELECT 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 connections c + JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id + WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL + |] + (userId, userId) getUserContactLinks :: DB.Connection -> VersionRangeChat -> User -> IO [(Connection, UserContact)] getUserContactLinks db vr User {userId} = @@ -531,6 +527,17 @@ userContactLinkQuery = FROM user_contact_links |] +setUserContactLinkShortLink :: DB.Connection -> Int64 -> ShortLinkContact -> IO () +setUserContactLinkShortLink db userContactLinkId shortLink = + DB.execute + db + [sql| + UPDATE user_contact_links + SET short_link_contact = ? + WHERE user_contact_link_id = ? + |] + (shortLink, userContactLinkId) + getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do ctId_ <- diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index b0e98856d9..36833ddd3d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -501,19 +501,6 @@ Query: Plan: SEARCH messages USING INDEX idx_messages_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) -Query: - SELECT 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 connections c - JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id - WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL - -Plan: -SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) - Query: SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items @@ -2823,6 +2810,19 @@ Plan: SEARCH uc USING INDEX idx_user_contact_links_group_id (group_id=?) SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) +Query: + SELECT 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 connections c + JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id + WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL + +Plan: +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + Query: SELECT 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,