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 ec3e58ca54..4ebfec1faa 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 @@ -1174,6 +1174,13 @@ object ChatController { return null } + suspend fun apiShareMyAddress(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, sendAsGroup: Boolean): MsgContent? { + val r = sendCmd(rh, CC.ApiShareMyAddress(toChatType, toChatId, toScope, sendAsGroup)) + if (r is API.Result && r.res is CR.ChatMsgContent) return r.res.msgContent + apiErrorAlert("apiShareMyAddress", generalGetString(MR.strings.error_sharing_address), r) + return null + } + suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List): CR.ForwardPlan? { val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds)) if (r is API.Result && r.res is CR.ForwardPlan) return r.res @@ -3809,6 +3816,7 @@ sealed class CC { class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List): CC() class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List, val ttl: Int?): CC() class ApiShareChatMsgContent(val shareChatType: ChatType, val shareChatId: Long, val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): CC() + class ApiShareMyAddress(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiNewPublicGroup(val userId: Long, val incognito: Boolean, val relayIds: List, val groupProfile: GroupProfile): CC() class ApiGetGroupRelays(val groupId: Long): CC() @@ -4015,6 +4023,7 @@ sealed class CC { is ApiShareChatMsgContent -> { "/_share chat content ${chatRef(shareChatType, shareChatId, null)} ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}" } + is ApiShareMyAddress -> "/_share address ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}" is ApiPlanForwardChatItems -> { "/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}" } @@ -4206,6 +4215,7 @@ sealed class CC { is ApiGetReactionMembers -> "apiGetReactionMembers" is ApiForwardChatItems -> "apiForwardChatItems" is ApiShareChatMsgContent -> "apiShareChatMsgContent" + is ApiShareMyAddress -> "apiShareMyAddress" is ApiPlanForwardChatItems -> "apiPlanForwardChatItems" is ApiNewGroup -> "apiNewGroup" is ApiNewPublicGroup -> "apiNewPublicGroup" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index fb00603166..4c84965b9e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -526,6 +526,7 @@ fun ComposeView( is SharedContent.Text -> emptyList() is SharedContent.Forward -> emptyList() is SharedContent.ChatLink -> emptyList() + is SharedContent.MyAddress -> emptyList() } // When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing chatModel.filesToDelete.removeAll { file -> @@ -1543,6 +1544,22 @@ fun ComposeView( } } } + is SharedContent.MyAddress -> { + val cInfo = chat.chatInfo + val sendAsGroup = cInfo.sendAsGroup + withBGApi { + val mc = chatModel.controller.apiShareMyAddress( + chat.remoteHostId, + cInfo.chatType, cInfo.apiId, + cInfo.groupChatScope(), sendAsGroup + ) + if (mc is MsgContent.MCChat) { + composeState.value = composeState.value.copy( + preview = ComposePreview.ChatLinkPreview(mc.chatLink, mc.ownerSig) + ) + } + } + } null -> {} } chatModel.sharedContent.value = null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 4e58501b19..382d126db7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -51,7 +51,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { } } } - is SharedContent.ChatLink -> { + is SharedContent.ChatLink, is SharedContent.MyAddress -> { hasSimplexLink = true } null -> {} @@ -99,9 +99,10 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal } val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val navButton: @Composable RowScope.() -> Unit = { + val sharedContent = remember { chatModel.sharedContent }.value when { showSearch -> NavigationButtonBack(hideSearchOnBack) - (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward && remember { chatModel.sharedContent }.value !is SharedContent.ChatLink -> { + (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && sharedContent !is SharedContent.Forward && sharedContent !is SharedContent.ChatLink && sharedContent !is SharedContent.MyAddress -> { val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } @@ -127,7 +128,6 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal } } else -> NavigationButtonBack(onButtonClicked = { - val sharedContent = chatModel.sharedContent.value // Drop shared content chatModel.sharedContent.value = null if (sharedContent is SharedContent.Forward) { @@ -150,6 +150,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal is SharedContent.File -> stringResource(MR.strings.share_file) is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message) is SharedContent.ChatLink -> stringResource(MR.strings.share_channel) + is SharedContent.MyAddress -> stringResource(MR.strings.share_address) null -> stringResource(MR.strings.share_message) }, color = MaterialTheme.colors.onBackground, @@ -196,7 +197,7 @@ private fun ShareList( val oneHandUI = remember { appPrefs.oneHandUI.state } val chats by remember(search) { derivedStateOf { - val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready && it.chatInfo.sendMsgEnabled && !(chatModel.sharedContent.value is SharedContent.ChatLink && it.chatInfo is ChatInfo.Local) }.sortedByDescending { it.chatInfo is ChatInfo.Local } + val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready && it.chatInfo.sendMsgEnabled && !((chatModel.sharedContent.value is SharedContent.ChatLink || chatModel.sharedContent.value is SharedContent.MyAddress) && it.chatInfo is ChatInfo.Local) }.sortedByDescending { it.chatInfo is ChatInfo.Local } filteredChats(mutableStateOf(false), mutableStateOf>(emptySet()), search, sorted) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt index cf3281f776..e1aa1f98dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt @@ -16,6 +16,7 @@ sealed class SharedContent { data class File(val text: String, val uri: URI): SharedContent() data class Forward(val chatItems: List, val fromChatInfo: ChatInfo): SharedContent() data class ChatLink(val groupInfo: GroupInfo): SharedContent() + object MyAddress: SharedContent() } enum class AnimatedViewState { 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 5394bf1040..87660c7e65 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 @@ -354,6 +354,14 @@ private fun UserAddressLayout( share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } } + ShareViaChatButton { + val shareViaChat = { + chatModel.sharedContent.value = SharedContent.MyAddress + chatModel.chatId.value = null + ModalManager.closeAllModalsEverywhere() + } + if (userAddress.shouldBeUpgraded) showAddShortLinkAlert { shareViaChat() } else shareViaChat() + } // ShareViaEmailButton { sendEmail(userAddress) } BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings) @@ -438,6 +446,17 @@ private fun AddShortLinkButton(text: String, onClick: () -> Unit) { ) } +@Composable +private fun ShareViaChatButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_forward), + stringResource(MR.strings.share_via_chat), + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun CreateOneTimeLinkButton() { val closeAll = { ModalManager.start.closeModals() } 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 b047bdb2c1..04d0a46d82 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -574,6 +574,7 @@ Forward message… Forward messages… Share channel… + Share address… Cannot send message Selected chat preferences prohibit this message. Share via chat @@ -586,6 +587,7 @@ (from owner) (signed) Error sharing channel + Error sharing address Link signature verified. ⚠️ Signature verification failed: %s. diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 693a467bbf..8426ab3cc5 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -398,6 +398,7 @@ data ChatCommand | APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId} | APIForwardChatItems {toChatRef :: ChatRef, sendAsGroup :: ShowGroupAsSender, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} | APIShareChatMsgContent {shareChatRef :: ChatRef, toSendRef :: SendRef} + | APIShareMyAddress {toSendRef :: SendRef} | APIUserRead UserId | UserRead | APIChatRead {chatRef :: ChatRef} @@ -563,6 +564,7 @@ data ChatCommand | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} | SharePublicGroup {shareGroupName :: GroupName, toChatName :: ChatName} + | ShareMyAddress {toChatName :: ChatName} | SendMessage SendName Text | SendMemberContactMessage GroupName ContactName Text | AcceptMemberContact ContactName diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index d20be74d28..3b579d3cc3 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1168,6 +1168,15 @@ processChatCommand cxt nm = \case | asGroup = (CBChannel, smpEncode pgId) | otherwise = (CBGroup, smpEncode (pgId, memberId)) APIShareChatMsgContent _ _ -> throwCmdError "sharing is only supported for public groups" + APIShareMyAddress _ -> withUser $ \user -> do + UserContactLink {connLinkContact = CCLink _ sl_, addressSettings} <- withFastStore (`getUserAddress` user) + case sl_ of + Nothing -> throwCmdError "your address has no short link to share" + Just connLink -> do + let business = businessAddress addressSettings + profile = userProfileDirect user Nothing Nothing True + text = safeDecodeUtf8 $ strEncode connLink + pure $ CRChatMsgContent user MCChat {text, chatLink = MCLContact {connLink, profile, business}, ownerSig = Nothing} APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of @@ -2447,6 +2456,18 @@ processChatCommand cxt nm = \case CRChatMsgContent _ mc -> processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage Nothing mc] r -> pure r + ShareMyAddress toChatName -> withUser $ \user -> do + toChatRef <- getChatRef user toChatName + sendRef <- case toChatRef of + ChatRef CTDirect ctId _ -> pure $ SRDirect ctId + ChatRef CTGroup gId scope_ -> do + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId + pure $ SRGroup gId scope_ (useRelays' gInfo) + _ -> throwCmdError "unsupported share target" + processChatCommand cxt nm (APIShareMyAddress sendRef) >>= \case + CRChatMsgContent _ mc -> + processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage Nothing mc] + r -> pure r SendMessage sendName msg -> withUser $ \user -> do let mc = MCText msg case sendName of @@ -5353,6 +5374,7 @@ chatCommandP = "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), "/_forward " *> (APIForwardChatItems <$> chatRefP <*> (" as_group=" *> onOffP <|> pure False) <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), "/_share chat content " *> (APIShareChatMsgContent <$> chatRefP <* A.space <*> sendRefP), + "/_share address " *> (APIShareMyAddress <$> sendRefP), "/_read user " *> (APIUserRead <$> A.decimal), "/read user" $> UserRead, "/_read chat " *> (APIChatRead <$> chatRefP), @@ -5550,6 +5572,7 @@ chatCommandP = ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, "/share chat #" *> (SharePublicGroup <$> displayNameP <* A.space <*> chatNameP), + "/share address " *> (ShareMyAddress <$> chatNameP), SendMessage <$> sendNameP <* A.space <*> msgTextP, "@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP), "/accept_member_contact @" *> (AcceptMemberContact <$> displayNameP),