From 3a38c39d3513361a121a4e0d3800302b61154fbc Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:42:47 +0000 Subject: [PATCH 01/21] desktop: fix remote desktop channel and connection issues (#7137) --- .../chat/simplex/common/model/SimpleXAPI.kt | 12 +++--- .../simplex/common/views/chat/ChatView.kt | 2 +- .../views/chat/group/AddGroupRelayView.kt | 8 ++-- .../views/chat/group/ChannelRelaysView.kt | 3 +- .../views/chat/group/ChannelWebPageView.kt | 2 +- .../common/views/newchat/AddChannelView.kt | 38 ++++++++++--------- .../common/views/newchat/NewChatSheet.kt | 2 +- 7 files changed, 37 insertions(+), 30 deletions(-) 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 54986e1815..02353e2c8a 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 @@ -1607,7 +1607,7 @@ object ChatController { suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? { val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData)) - if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat + if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh) Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}") return null @@ -1616,7 +1616,7 @@ object ChatController { suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData): Chat? { val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData)) - if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat + if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh) Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}") return null @@ -2172,8 +2172,8 @@ object ChatController { return null } - suspend fun apiGetGroupRelays(groupId: Long): List { - val r = sendCmd(null, CC.ApiGetGroupRelays(groupId)) + suspend fun apiGetGroupRelays(rh: Long?, groupId: Long): List { + val r = sendCmd(rh, CC.ApiGetGroupRelays(groupId)) if (r is API.Result && r.res is CR.GroupRelays) return r.res.groupRelays return emptyList() } @@ -2183,8 +2183,8 @@ object ChatController { data class AddFailed(val addRelayResults: List): AddGroupRelaysResult() } - suspend fun apiAddGroupRelays(groupId: Long, relayIds: List): AddGroupRelaysResult? { - val r = sendCmdWithRetry(null, CC.ApiAddGroupRelays(groupId, relayIds)) + suspend fun apiAddGroupRelays(rh: Long?, groupId: Long, relayIds: List): AddGroupRelaysResult? { + val r = sendCmdWithRetry(rh, CC.ApiAddGroupRelays(groupId, relayIds)) if (r is API.Result && r.res is CR.GroupRelaysAdded) return AddGroupRelaysResult.Added(r.res.groupInfo, r.res.groupLink, r.res.groupRelays) if (r is API.Result && r.res is CR.GroupRelaysAddFailed) return AddGroupRelaysResult.AddFailed(r.res.addRelayResults) if (r != null) throw Exception("${r.responseType}: ${r.details}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index cc9e71354c..765759d9e0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -208,7 +208,7 @@ fun ChatView( withBGApi { setGroupMembers(chatRh, cInfo.groupInfo, chatModel) if (cInfo.groupInfo.membership.memberRole == GroupMemberRole.Owner) { - val relays = chatModel.controller.apiGetGroupRelays(cInfo.groupInfo.groupId) + val relays = chatModel.controller.apiGetGroupRelays(chatRh, cInfo.groupInfo.groupId) withContext(Dispatchers.Main) { ChannelRelaysModel.set(cInfo.groupInfo.groupId, relays) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt index 1ed75bd2a2..95ce003caa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt @@ -31,6 +31,7 @@ data class AvailableRelay( @Composable fun AddGroupRelayView( + rhId: Long?, groupInfo: GroupInfo, existingRelayIds: Set, onRelayAdded: () -> Unit, @@ -46,7 +47,7 @@ fun AddGroupRelayView( LaunchedEffect(Unit) { try { - val servers = ChatController.getUserServers(null) + val servers = ChatController.getUserServers(rhId) if (servers != null) { val relays = mutableListOf() for (op in servers) { @@ -80,7 +81,7 @@ fun AddGroupRelayView( if (relayIds.isEmpty()) return@AddGroupRelayLayout isAdding = true scope.launch { - addSelectedRelays(groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays -> + addSelectedRelays(rhId, groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays -> selectedRelayIds = newSelectedIds availableRelays = newAvailableRelays isAdding = false @@ -183,6 +184,7 @@ private fun AddRelaysButton(onClick: () -> Unit, disabled: Boolean) { } private suspend fun addSelectedRelays( + rhId: Long?, groupInfo: GroupInfo, relayIds: List, selectedRelayIds: Set, @@ -192,7 +194,7 @@ private suspend fun addSelectedRelays( updateState: (Set, List) -> Unit ) { try { - val result = ChatController.apiAddGroupRelays(groupInfo.groupId, relayIds) + val result = ChatController.apiAddGroupRelays(rhId, groupInfo.groupId, relayIds) if (result == null) { updateState(selectedRelayIds, availableRelays) return diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt index d7ffde71c8..60cc19bb1b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt @@ -37,7 +37,7 @@ fun ChannelRelaysView( LaunchedEffect(Unit) { setGroupMembers(rhId, groupInfo, chatModel) if (groupInfo.isOwner) { - val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId) + val relays = chatModel.controller.apiGetGroupRelays(rhId, groupInfo.groupId) ChannelRelaysModel.set(groupId = groupInfo.groupId, groupRelays = relays) } } @@ -114,6 +114,7 @@ private fun ChannelRelaysLayout( val existingRelayIds = groupRelays.mapNotNull { it.userChatRelay.chatRelayId }.toSet() ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close -> AddGroupRelayView( + rhId = rhId, groupInfo = groupInfo, existingRelayIds = existingRelayIds, onRelayAdded = { withBGApi { setGroupMembers(rhId, groupInfo, chatModel) } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt index 98067e49dd..262a78b3c8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt @@ -81,7 +81,7 @@ fun ChannelWebPageView( } LaunchedEffect(Unit) { - val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId) + val relays = chatModel.controller.apiGetGroupRelays(rhId, groupInfo.groupId) groupRelays.clear() groupRelays.addAll(relays) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt index b5188178fa..639a5cc78e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt @@ -38,7 +38,8 @@ import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.* @Composable -fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit) { +fun AddChannelView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) { + val rhId = rh?.remoteHostId val view = LocalMultiplatformView() val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() @@ -56,7 +57,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit val gInfo = groupInfo.value if (showLinkStep.value && gInfo != null) { - LinkStepView(chatModel, gInfo, groupLink, closeAll) + LinkStepView(chatModel, rhId, gInfo, groupLink, closeAll) } else if (gInfo != null) { ProgressStepView( chatModel, gInfo, groupRelays, relayListExpanded, @@ -65,9 +66,9 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit chatModel.creatingChannelId.value = null closeAll() withBGApi { - openGroupChat(null, gInfo.groupId) + openGroupChat(rhId, gInfo.groupId) ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close -> - GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close) + GroupLinkView(chatModel, rhId = rhId, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close) } } } @@ -80,9 +81,9 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit closeAll() withBGApi { try { - chatModel.controller.apiDeleteChat(rh = null, type = ChatType.Group, id = gInfo.apiId) + chatModel.controller.apiDeleteChat(rh = rhId, type = ChatType.Group, id = gInfo.apiId) withContext(Dispatchers.Main) { - chatModel.chatsContext.removeChat(null, gInfo.id) + chatModel.chatsContext.removeChat(rhId, gInfo.id) } } catch (e: Exception) { Log.e(TAG, "cancelChannelCreation error: ${e.message}") @@ -93,6 +94,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit } else { ProfileStepView( chatModel = chatModel, + rhId = rhId, displayName = displayName, profileImage = profileImage, chosenImage = chosenImage, @@ -120,7 +122,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit creationInProgress.value = true withBGApi { try { - val enabledRelays = chooseRandomRelays() + val enabledRelays = chooseRandomRelays(rhId) val relayIds = enabledRelays.mapNotNull { it.chatRelayId } if (relayIds.isEmpty()) { withContext(Dispatchers.Main) { @@ -130,7 +132,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit return@withBGApi } val result = chatModel.controller.apiNewPublicGroup( - rh = null, + rh = rhId, incognito = false, relayIds = relayIds, groupProfile = profile @@ -138,7 +140,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit when (result) { is ChatController.PublicGroupCreationResult.Created -> { withContext(Dispatchers.Main) { - chatModel.chatsContext.updateGroup(rhId = null, result.groupInfo) + chatModel.chatsContext.updateGroup(rhId = rhId, result.groupInfo) chatModel.creatingChannelId.value = result.groupInfo.id groupInfo.value = result.groupInfo groupLink.value = result.groupLink @@ -178,8 +180,8 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit private const val maxRelays = 3 -private suspend fun chooseRandomRelays(): List { - val servers = getUserServers(rh = null) ?: return emptyList() +private suspend fun chooseRandomRelays(rhId: Long?): List { + val servers = getUserServers(rh = rhId) ?: return emptyList() // Operator relays are grouped per operator; custom relays (null operator) // are treated independently to maximize trust distribution. val operatorGroups = mutableListOf>() @@ -215,8 +217,8 @@ private suspend fun chooseRandomRelays(): List { return selected } -private suspend fun checkHasRelays(): Boolean { - val servers = try { getUserServers(rh = null) } catch (_: Exception) { null } ?: return false +private suspend fun checkHasRelays(rhId: Long?): Boolean { + val servers = try { getUserServers(rh = rhId) } catch (_: Exception) { null } ?: return false return servers.any { op -> (op.operator?.enabled ?: true) && op.chatRelays.any { it.enabled && !it.deleted && it.chatRelayId != null } @@ -226,6 +228,7 @@ private suspend fun checkHasRelays(): Boolean { @Composable private fun ProfileStepView( chatModel: ChatModel, + rhId: Long?, displayName: MutableState, profileImage: MutableState, chosenImage: MutableState, @@ -239,7 +242,7 @@ private fun ProfileStepView( createChannel: () -> Unit ) { LaunchedEffect(Unit) { - hasRelays.value = checkHasRelays() + hasRelays.value = checkHasRelays(rhId) } ModalBottomSheetLayout( @@ -553,6 +556,7 @@ private fun RelayRow(relay: GroupRelay, connFailed: Boolean) { @Composable private fun LinkStepView( chatModel: ChatModel, + rhId: Long?, gInfo: GroupInfo, groupLink: MutableState, closeAll: () -> Unit @@ -563,14 +567,14 @@ private fun LinkStepView( delay(500) withContext(Dispatchers.Main) { ModalManager.start.closeModals() - openGroupChat(null, gInfo.groupId) + openGroupChat(rhId, gInfo.groupId) } } } ModalView(close = close, showClose = false, cardScreen = true) { GroupLinkView( chatModel = chatModel, - rhId = null, + rhId = rhId, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = { groupLink.value = it }, @@ -660,6 +664,6 @@ fun RelayProgressIndicator(active: Int, total: Int) { @Composable fun PreviewAddChannelView() { SimpleXTheme { - AddChannelView(chatModel = ChatModel, close = {}, closeAll = {}) + AddChannelView(chatModel = ChatModel, rh = null, close = {}, closeAll = {}) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index af7f59496b..68f42f4186 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -64,7 +64,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } }, createChannel = { - ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, close, closeAll) } + ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } }, rh = rh, close = close From e979b7efdcfe74e30d4025102366ab29b6f25d81 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:12:39 +0000 Subject: [PATCH 02/21] android, desktop, ios: remove left padding on consecutive received messages in channels (#7108) * android, desktop, ios: remove left padding on consecutive received messages in channels In channels, a received message that does not show an avatar (a consecutive post from the same sender) drops the avatar-sized left padding and sits flush-left. Applies to both owner broadcasts (ChannelRcv) and contributor posts (GroupRcv); the first message of each run still shows the avatar. Gated on ChatInfo.isChannel, so regular groups, business and direct chats, sent messages, and avatar-shown messages are unchanged. * docs: add plan justifying removing left padding on consecutive received messages in channels * ios: fix right gap on consecutive received messages in channels Removing the avatar-sized left padding from no-avatar received messages (this PR) shifted those bubbles ~44pt left, but maxWidth still reserved the avatar inset, so consecutive messages stopped ~44pt short of the first (avatar) message on the right. Widen maxWidth for no-avatar channel-received items so their right edge matches the avatar-shown first message. The no-avatar predicate reuses the exact shouldShowAvatar expression from the render path (lifted to a file-scope function so the maxWidth site can call it), so the width and the rendered layout can never disagree. Android is unaffected: Compose derives content width from padding, so reducing the start padding already widened the row there. * ios: increase left padding * kotlin: increase left gap --------- Co-authored-by: Evgeny Poberezkin Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Views/Chat/ChatView.swift | 58 +++++----- .../simplex/common/views/chat/ChatView.kt | 4 +- ...channel-received-no-avatar-left-padding.md | 103 ++++++++++++++++++ 3 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 plans/2026-06-20-channel-received-no-avatar-left-padding.md diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index ded0019692..283157864d 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -14,6 +14,29 @@ import Combine private let memberImageSize: CGFloat = 34 +private func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool { + let oldIsGroupRcv = switch older?.chatDir { + case .groupRcv: true + case .channelRcv: true + default: false + } + let sameMember = switch (older?.chatDir, current.chatDir) { + case (.groupRcv(let oldMember), .groupRcv(let member)): + oldMember.memberId == member.memberId + case (.channelRcv, .channelRcv): + true + default: + false + } + if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { + return true + } else if case .channelRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { + return true + } else { + return false + } +} + // Spec: spec/client/chat-view.md#ChatView struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @@ -896,8 +919,14 @@ struct ChatView: View { } else { let voiceNoFrame = voiceWithoutFrame(ci) let channelReceived = !ci.chatDir.sent && cInfo.isChannel + // consecutive (no-avatar) received messages in channels drop the avatar-sized + // left padding (see .leading padding below), so they get the full row width here + // too — otherwise the reserved avatar inset would leave a gap on the right + let channelReceivedNoAvatar = channelReceived && !shouldShowAvatar(mergedItem.newest().item, mergedItem.oldest().nextItem) let maxWidth = cInfo.chatType == .group - ? voiceNoFrame || channelReceived + ? channelReceivedNoAvatar + ? g.size.width - 26 + : voiceNoFrame || channelReceived ? (g.size.width - 28) - 42 : (g.size.width - 28) * 0.84 - 42 : voiceNoFrame @@ -1733,29 +1762,6 @@ struct ChatView: View { ) } - func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool { - let oldIsGroupRcv = switch older?.chatDir { - case .groupRcv: true - case .channelRcv: true - default: false - } - let sameMember = switch (older?.chatDir, current.chatDir) { - case (.groupRcv(let oldMember), .groupRcv(let member)): - oldMember.memberId == member.memberId - case (.channelRcv, .channelRcv): - true - default: - false - } - if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { - return true - } else if case .channelRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { - return true - } else { - return false - } - } - var body: some View { let last = isLastItem ? im.reversedChatItems.last : nil let listItem = merged.newest() @@ -1979,7 +1985,7 @@ struct ChatView: View { } chatItemWithMenu(ci, range, maxWidth, itemSeparation) .padding(.trailing) - .padding(.leading, 10 + memberImageSize + 12) + .padding(.leading, chat.chatInfo.isChannel ? nil : 10 + memberImageSize + 12) } .padding(.bottom, bottomPadding) } @@ -2076,7 +2082,7 @@ struct ChatView: View { } chatItemWithMenu(ci, range, maxWidth, itemSeparation) .padding(.trailing) - .padding(.leading, 10 + memberImageSize + 12) + .padding(.leading, chat.chatInfo.isChannel ? nil : 10 + memberImageSize + 12) } .padding(.bottom, bottomPadding) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 765759d9e0..4a3bfe4208 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -2084,7 +2084,7 @@ fun BoxScope.ChatItemsList( } Row( Modifier - .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = if (chatInfo.isChannel) 12.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) .then(swipeableOrSelectionModifier) ) { @@ -2167,7 +2167,7 @@ fun BoxScope.ChatItemsList( } Row( Modifier - .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = if (chatInfo.isChannel) 12.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) .then(swipeableOrSelectionModifier) ) { diff --git a/plans/2026-06-20-channel-received-no-avatar-left-padding.md b/plans/2026-06-20-channel-received-no-avatar-left-padding.md new file mode 100644 index 0000000000..13ee4ebc4f --- /dev/null +++ b/plans/2026-06-20-channel-received-no-avatar-left-padding.md @@ -0,0 +1,103 @@ +# Remove left padding on consecutive (no-avatar) received messages in channels + +## Problem + +In a channel, received messages show the sender avatar on the first message of a +run and hide it on consecutive messages, but those consecutive messages still +reserve the avatar-sized **left padding** so they line up under the first. For a +channel's feed-style layout this indentation wastes horizontal space — +consecutive received messages should sit flush-left where the avatar would be. +This applies to **both** the channel owner's broadcasts and contributors' posts. + +Desired behaviour: in channels, any received message that does **not** show an +avatar (a consecutive post from the same sender) drops the avatar-sized left +padding. The first message of a run still shows the avatar and keeps its layout; +when the run is broken (a different sender, or a time gap), the next message +shows the avatar again — this run logic is unchanged, only the no-avatar left +padding is reduced. + +## The two received directions in a channel + +Received items in a channel arrive as one of two directions +(`Subscriber.hs`, `saveRcvCI`): + +- **`ChannelRcv`** (no member) — the **owner's** broadcast, sent "as the channel". + The backend permits sending-as-group only to the owner, and a channel owner's + main-scope messages are always sent as group (`ChatInfo.sendAsGroup` is true + for `useRelays && memberRole >= Owner` in the main scope), so received owner + posts arrive as `ChannelRcv`. Shows the channel avatar. +- **`GroupRcv(member)`** (attributed) — a **contributor's** post, carrying the + member. Shows the member avatar. + +Both are received messages, and the change now applies to **both** when they hide +the avatar. (An earlier revision scoped this to `ChannelRcv`/owner only; it now +covers contributors too, per request.) + +## Change + +In channels — gated on `ChatInfo.isChannel` (the `useRelays` flag, which is +reliably present, unlike the optional group-type predicate) — the no-avatar +branches for **both** `ChannelRcv` and `GroupRcv` drop the avatar-sized left +padding down to the base inset where the avatar itself starts. In non-channel +groups the `GroupRcv` no-avatar layout is unchanged (`isChannel` is false). The +avatar-shown layouts, sent messages, and all other chats are unchanged. + +The same Row's `end` padding is already gated on `chatInfo.isChannel` (the merged +right-gap change #7106), so gating `start` on `isChannel` keeps each Row +internally consistent and the change precisely "in channels". + +### Android / desktop (`apps/multiplatform`, `ChatView.kt`, `ChatItemsList`) + +Both the `CIDirection.GroupRcv` and `CIDirection.ChannelRcv` `showAvatar == false` +rows: + +```kotlin +// before +.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = …) +// after +.padding(start = if (chatInfo.isChannel) 8.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = …) +``` + +### iOS (`apps/ios`, `ChatView.swift`, `chatItemListView`) + +Both the `.groupRcv` and `.channelRcv` no-avatar branches: + +```swift +// before +.padding(.leading, 10 + memberImageSize + 12) +// after +.padding(.leading, chat.chatInfo.isChannel ? 12 : 10 + memberImageSize + 12) +``` + +## Run behaviour (unchanged) + +`shouldShowAvatar(current, older)` shows the avatar on the first message of a +same-sender run and hides it on consecutive ones; a different sender or a gap +resets the run. For `GroupRcv` "same sender" is the same `memberId`; for +`ChannelRcv` consecutive channel broadcasts count as the same sender. Only the +no-avatar left padding is changed. + +## Scope + +- Affects: all received consecutive (no-avatar) messages **in channels** — owner + broadcasts (`ChannelRcv`) and contributor posts (`GroupRcv`). This includes a + channel's member-support sub-scope, which renders through the same + `ChatItemsList` with the channel's `isChannel`; treating it the same way is + consistent with the merged right-gap change (#7106), which also gates that + Row's `end` padding on `isChannel` without a scope filter. +- Unchanged: the first message of each run (avatar shown), sent messages, regular + groups, business chats and direct chats (`isChannel` false — the `else` branch + preserves the original avatar-inset value exactly), and any non-channel + `ChannelRcv` welcome item. + +## Verification + +- Android/desktop: `:common:compileKotlinDesktop` compiles clean. +- iOS: small, type-safe constant change; build/verify on macOS (Xcode) — not + compilable on the Linux build host used here. +- Visual (both platforms), in a channel: + - First message of a run (owner or contributor): avatar shown, layout unchanged. + - Following messages from the same sender (no avatar): now flush-left. + - A different sender / time gap resets the run — the next message shows the + avatar again. + - Regular groups, business and direct chats keep their existing indentation. From 894e44b8ed8ad7fb5383ad84518663f1996bfdad Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:12:47 +0000 Subject: [PATCH 03/21] ui: fix channel contributor being able to "send message" without relays (#7139) --- .../Chat/ComposeMessage/ComposeView.swift | 48 +++++++++++-------- apps/ios/SimpleXChat/ChatTypes.swift | 2 +- .../chat/simplex/common/model/ChatModel.kt | 6 +-- .../simplex/common/views/chat/ComposeView.kt | 48 ++++++++++++++----- 4 files changed, 68 insertions(+), 36 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index e308a145b9..9c40b2b395 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -392,38 +392,31 @@ struct ComposeView: View { } let ownerState = ownerRelayState + let subscriberState = subscriberRelayState if let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays, ![.memRejected, .memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) { if gInfo.membership.memberRole == .owner { if let s = ownerState, s.relays.isEmpty || s.activeCount < s.relays.count { ownerChannelRelayBar(relays: s.relays, activeCount: s.activeCount, failedCount: s.failedCount, removedCount: s.removedCount) } - } else { - let hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?? []).sorted() - let relayMembers = chatModel.groupMembers - .filter { $0.wrapped.memberRole == .relay && ![.memRemoved, .memGroupDeleted].contains($0.wrapped.memberStatus) } - .sorted { hostFromRelayLink($0.wrapped.relayLink ?? "") < hostFromRelayLink($1.wrapped.relayLink ?? "") } + } else if let s = subscriberState { let showProgress = !gInfo.nextConnectPrepared || composeState.inProgress - let removedCount = relayMembers.filter { relayMemberRemoved($0.wrapped.memberStatus) }.count - let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count - let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count - let resolvedCount = connectedCount + removedCount + failedCount - let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count - if total == 0 || removedCount + failedCount > 0 || resolvedCount < total { + let resolvedCount = s.connectedCount + s.removedCount + s.failedCount + if s.total == 0 || s.removedCount + s.failedCount > 0 || resolvedCount < s.total { subscriberChannelRelayBar( - hostnames: hostnames, - relayMembers: relayMembers, - connectedCount: connectedCount, - removedCount: removedCount, - failedCount: failedCount, - total: total, + hostnames: s.hostnames, + relayMembers: s.relayMembers, + connectedCount: s.connectedCount, + removedCount: s.removedCount, + failedCount: s.failedCount, + total: s.total, showProgress: showProgress ) } } } - let userCantSendReason = chat.chatInfo.userCantSendReason(allRelaysBroken: ownerState?.noActiveRelays ?? false) + let userCantSendReason = chat.chatInfo.userCantSendReason(allRelaysBroken: (ownerState?.noActiveRelays ?? subscriberState?.noActiveRelays) ?? false) let composeEnabled = ( userCantSendReason == nil || (chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) || @@ -748,8 +741,25 @@ struct ComposeView: View { return (relays, activeCount, failedCount, removedCount, noActiveRelays) } + private var subscriberRelayState: (hostnames: [String], relayMembers: [GMember], connectedCount: Int, removedCount: Int, failedCount: Int, total: Int, noActiveRelays: Bool)? { + guard let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays, + gInfo.membership.memberRole != .owner, + ![.memRejected, .memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) + else { return nil } + let hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?? []).sorted() + let relayMembers = chatModel.groupMembers + .filter { $0.wrapped.memberRole == .relay && ![.memRemoved, .memGroupDeleted].contains($0.wrapped.memberStatus) } + .sorted { hostFromRelayLink($0.wrapped.relayLink ?? "") < hostFromRelayLink($1.wrapped.relayLink ?? "") } + let removedCount = relayMembers.filter { relayMemberRemoved($0.wrapped.memberStatus) }.count + let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count + let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count + let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count + let noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total + return (hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays) + } + private var disabledText: LocalizedStringKey? { - chat.chatInfo.userCantSendReason(allRelaysBroken: ownerRelayState?.noActiveRelays ?? false)?.composeLabel + chat.chatInfo.userCantSendReason(allRelaysBroken: (ownerRelayState?.noActiveRelays ?? subscriberRelayState?.noActiveRelays) ?? false)?.composeLabel } @ViewBuilder private func ownerChannelRelayBar(relays: [GroupRelay], activeCount: Int, failedCount: Int, removedCount: Int) -> some View { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 8b186f168d..7a20a8d4ba 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1720,11 +1720,11 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { if groupInfo.membership.memberActive { switch(groupChatScope) { case .none: - if allRelaysBroken && groupInfo.useRelays { return ("can't broadcast", nil) } if groupInfo.membership.memberPending { return ("reviewed by admins", "Please contact group admin.") } if groupInfo.membership.memberRole == .observer { return groupInfo.useRelays ? ("you are subscriber", nil) : ("you are observer", "Please contact group admin.") } + if allRelaysBroken && groupInfo.useRelays { return ("can't broadcast", nil) } return nil case let .some(.memberSupport(groupMember_: .some(supportMember))): if supportMember.versionRange.maxVersion < GROUP_KNOCKING_VERSION && !supportMember.memberPending { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index c5dd81fec7..44361baa73 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1660,9 +1660,6 @@ sealed class ChatInfo: SomeChat, NamedChat { if (groupInfo.membership.memberActive) { when (groupChatScope) { null -> { - if (allRelaysBroken && groupInfo.useRelays) { - return generalGetString(MR.strings.cant_broadcast_message) to null - } if (groupInfo.membership.memberPending) { return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc) } @@ -1673,6 +1670,9 @@ sealed class ChatInfo: SomeChat, NamedChat { generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc) } } + if (allRelaysBroken && groupInfo.useRelays) { + return generalGetString(MR.strings.cant_broadcast_message) to null + } return null } is GroupChatScopeInfo.MemberSupport -> 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 21290b99b8..c37aa0a77f 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 @@ -1203,8 +1203,9 @@ fun ComposeView( } val ownerRelayState = ownerRelayState(chat, chatModel) + val subscriberRelayState = subscriberRelayState(chat, chatModel) - val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason(ownerRelayState?.noActiveRelays == true)) + val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason((ownerRelayState?.noActiveRelays ?: subscriberRelayState?.noActiveRelays) == true)) val sendMsgEnabled = rememberUpdatedState(userCantSendReason.value == null) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) @@ -1575,18 +1576,12 @@ fun ComposeView( } } } else { - val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted() - val relayMembers = chatModel.groupMembers.value - .filter { it.memberRole == GroupMemberRole.Relay && it.memberStatus !in listOf(GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) } - .sortedBy { hostFromRelayLink(it.relayLink ?: "") } - val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress - val removedCount = relayMembers.count { relayMemberRemoved(it.memberStatus) } - val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null } - val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null } - val resolvedCount = connectedCount + removedCount + failedCount - val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size - if (total == 0 || removedCount + failedCount > 0 || resolvedCount < total) { - SubscriberChannelRelayBar(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, showProgress, relayListExpanded) + subscriberRelayState?.let { s -> + val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress + val resolvedCount = s.connectedCount + s.removedCount + s.failedCount + if (s.total == 0 || s.removedCount + s.failedCount > 0 || resolvedCount < s.total) { + SubscriberChannelRelayBar(s.hostnames, s.relayMembers, s.connectedCount, s.removedCount, s.failedCount, s.total, showProgress, relayListExpanded) + } } } } @@ -2052,6 +2047,33 @@ private data class OwnerRelayState( val noActiveRelays: Boolean ) +private fun subscriberRelayState(chat: Chat, chatModel: ChatModel): SubscriberRelayState? { + val gInfo = (chat.chatInfo as? ChatInfo.Group)?.groupInfo ?: return null + if (!gInfo.useRelays || gInfo.membership.memberRole == GroupMemberRole.Owner || + gInfo.membership.memberStatus in listOf(GroupMemberStatus.MemRejected, GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) + ) return null + val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted() + val relayMembers = chatModel.groupMembers.value + .filter { it.memberRole == GroupMemberRole.Relay && it.memberStatus !in listOf(GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) } + .sortedBy { hostFromRelayLink(it.relayLink ?: "") } + val removedCount = relayMembers.count { relayMemberRemoved(it.memberStatus) } + val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null } + val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null } + val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size + val noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total + return SubscriberRelayState(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays) +} + +private data class SubscriberRelayState( + val hostnames: List, + val relayMembers: List, + val connectedCount: Int, + val removedCount: Int, + val failedCount: Int, + val total: Int, + val noActiveRelays: Boolean +) + private fun relayMemberRemoved(status: GroupMemberStatus?): Boolean = status in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) From 6816e1a1ff1c4a71f8fbc428aead68deee5facf9 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:48:00 +0000 Subject: [PATCH 04/21] desktop, android: improve custom relay and user address text fields design (#7155) --- .../simplex/common/views/helpers/Section.kt | 10 ++-- .../common/views/helpers/TextEditor.kt | 3 +- .../views/usersettings/UserAddressView.kt | 9 +++- .../networkAndServers/ChatRelayView.kt | 51 ++++++++----------- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index 9afcdd0b94..5196a144b1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -1,4 +1,5 @@ import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -27,7 +28,7 @@ import chat.simplex.common.views.onboarding.SelectableCard import chat.simplex.common.views.usersettings.SettingsActionItemWithContent import chat.simplex.res.MR -private val SectionCardShape = RoundedCornerShape(16.dp) +val SectionCardShape = RoundedCornerShape(16.dp) val CARD_PADDING = 18.dp val ICON_TEXT_SPACING = 8.dp @@ -113,15 +114,18 @@ fun SectionView( iconTint: Color = MaterialTheme.colors.secondary, leadingIcon: Boolean = false, padding: PaddingValues = PaddingValues(), + onIconClick: (() -> Unit)? = null, content: (@Composable ColumnScope.() -> Unit) ) { val card = LocalCardScreen.current Column { val iconSize = with(LocalDensity.current) { 21.sp.toDp() } + val interactionSource = remember { MutableInteractionSource() } + val iconClickable = if (onIconClick != null) Modifier.clickable(interactionSource = interactionSource, indication = ripple(bounded = false, radius = iconSize * 0.75f), onClick = onIconClick) else Modifier Row(Modifier.padding(start = if (card) DEFAULT_PADDING + DEFAULT_PADDING_HALF else DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) { - if (leadingIcon) Icon(icon, null, Modifier.padding(end = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint) + if (leadingIcon) Icon(icon, null, Modifier.padding(end = DEFAULT_PADDING_HALF).size(iconSize).then(iconClickable), tint = iconTint) Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = if (card) 14.sp else 12.sp, fontWeight = if (card) FontWeight.Medium else FontWeight.Normal) - if (!leadingIcon) Icon(icon, null, Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint) + if (!leadingIcon) Icon(icon, null, Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize).then(iconClickable), tint = iconTint) } CardColumn(padding) { content() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index cd40585cad..833f53f2af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -31,6 +31,7 @@ fun TextEditor( modifier: Modifier, placeholder: String? = null, contentPadding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING), + shape: Shape = RoundedCornerShape(14.dp), isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null, enabled: Boolean = true @@ -53,7 +54,7 @@ fun TextEditor( .fillMaxWidth() .padding(contentPadding) .heightIn(min = 52.dp) - .border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(14.dp)), + .border(border = BorderStroke(1.dp, strokeColor), shape = shape), contentAlignment = Alignment.Center, ) { val textFieldModifier = modifier 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 c55eaf6c10..36c74cceb6 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 @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionCardShape import SectionDividerSpaced import SectionItemView import SectionTextFooter @@ -699,7 +700,13 @@ private fun AcceptIncognitoToggle(addressSettingsState: MutableState) { val autoReply = rememberSaveable { mutableStateOf(addressSettingsState.value.autoReply) } - TextEditor(autoReply, Modifier.height(100.dp), placeholder = stringResource(MR.strings.enter_welcome_message_optional)) + TextEditor( + autoReply, + Modifier.height(100.dp), + placeholder = stringResource(MR.strings.enter_welcome_message_optional), + contentPadding = PaddingValues(), + shape = SectionCardShape + ) LaunchedEffect(autoReply.value) { if (autoReply.value != addressSettingsState.value.autoReply) { addressSettingsState.value = AddressSettingsState( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt index ab63067226..9a2d7f8e61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings.networkAndServers import SectionBottomSpacer +import SectionCardShape import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween @@ -10,9 +11,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.sp import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource @@ -230,43 +229,35 @@ private fun CustomRelay( } SectionView( - stringResource(MR.strings.your_relay_address).uppercase(), + stringResource(MR.strings.your_relay_address), icon = painterResource(MR.images.ic_error), iconTint = if (!validAddress.value) MaterialTheme.colors.error else Color.Transparent, ) { TextEditor( relayAddress, - Modifier.height(144.dp) + Modifier.height(144.dp), + contentPadding = PaddingValues(), + shape = SectionCardShape ) } SectionDividerSpaced(maxTopPadding = true) - Column { - val iconSize = with(LocalDensity.current) { 21.sp.toDp() } - Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) { - Text( - stringResource(MR.strings.your_relay_name).uppercase(), - color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp - ) - IconButton( - onClick = { if (!validName.value) showInvalidRelayNameAlert(relayName) }, - enabled = !validName.value, - modifier = Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize) - ) { - Icon( - painterResource(MR.images.ic_error), null, - tint = if (!validName.value) MaterialTheme.colors.error else Color.Transparent - ) - } - } - Column(Modifier.fillMaxWidth()) { - TextEditor( - relayName, - Modifier, - placeholder = generalGetString(MR.strings.enter_relay_name), - enabled = relay.value.tested != true - ) - } + SectionView( + stringResource(MR.strings.your_relay_name), + icon = painterResource(MR.images.ic_error), + iconTint = if (!validName.value) MaterialTheme.colors.error else Color.Transparent, + onIconClick = if (!validName.value) { + { showInvalidRelayNameAlert(relayName) } + } else null + ) { + TextEditor( + relayName, + Modifier, + placeholder = generalGetString(MR.strings.enter_relay_name), + contentPadding = PaddingValues(), + shape = SectionCardShape, + enabled = relay.value.tested != true + ) } if (relay.value.tested != true) { SectionTextFooter(annotatedStringResource(MR.strings.test_relay_to_retrieve_name)) From 936e132c51d1720876196523cad722a6cfe9e03a Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:48:12 +0000 Subject: [PATCH 05/21] desktop: hide "Calls on lock screen" setting (#7154) --- .../simplex/common/views/usersettings/CallSettings.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt index cb36e4ae1a..43863d5794 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.appPlatform import chat.simplex.res.MR @Composable @@ -38,12 +39,14 @@ fun CallSettingsLayout( ) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_calls)) - val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } SectionView(stringResource(MR.strings.settings_section_title_settings)) { SectionItemView(editIceServers) { Text(stringResource(MR.strings.webrtc_ice_servers)) } - val enabled = remember { mutableStateOf(true) } - LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it }) + if (appPlatform.isAndroid) { + val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } + val enabled = remember { mutableStateOf(true) } + LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it }) + } SettingsPreferenceItem(null, stringResource(MR.strings.always_use_relay), webrtcPolicyRelay) } SectionTextFooter( From 249c5495dac963370379f82f25fb053468382b57 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:48:29 +0000 Subject: [PATCH 06/21] desktop, android: fix file chat item meta layout (#7156) --- .../common/views/chat/item/CIFileView.kt | 25 +++++++++++++------ .../common/views/chat/item/FramedItemView.kt | 2 +- .../common/views/chatlist/ChatPreviewView.kt | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 02bee37c24..b2b89ca041 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -15,7 +15,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -30,7 +32,10 @@ import java.net.URI @Composable fun CIFileView( file: CIFile?, - edited: Boolean, + meta: CIMeta, + chatTTL: Int?, + showViaProxy: Boolean, + showTimestamp: Boolean, showMenu: MutableState, smallView: Boolean = false, senderProfile: LocalProfile?, @@ -202,10 +207,13 @@ fun CIFileView( ) { fileIndicator() if (!smallView) { - val metaReserve = if (edited) - " " - else - " " + val secondaryColor = MaterialTheme.colors.secondary + val encrypted = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null + val metaReserve = buildAnnotatedString { + withStyle(reserveTimestampStyle) { + append(reserveSpaceForMeta(meta, chatTTL, encrypted, secondaryColor = secondaryColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp)) + } + } if (file != null) { Column { Text( @@ -213,8 +221,11 @@ fun CIFileView( maxLines = 1 ) Text( - formatBytes(file.fileSize) + metaReserve, - color = MaterialTheme.colors.secondary, + buildAnnotatedString { + append(formatBytes(file.fileSize)) + append(metaReserve) + }, + color = secondaryColor, fontSize = 14.sp, maxLines = 1 ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 5c07fe3abf..2e1db8928e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -201,7 +201,7 @@ fun FramedItemView( @Composable fun ciFileView(ci: ChatItem, text: String) { - CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile) + CIFileView(ci.file, ci.meta, chatTTL, showViaProxy, showTimestamp, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile) if (text != "" || ci.meta.isLive) { CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 2c7e443b4d..fbc4c7e336 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -342,7 +342,7 @@ fun ChatPreviewView( } } is MsgContent.MCFile -> SmallContentPreviewFile { - CIFileView(ci.file, false, remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) { + CIFileView(ci.file, ci.meta, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, showMenu = remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) { val user = chatModel.currentUser.value ?: return@CIFileView withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } } From 4da6e99d38fc081fedaa2959680106acb421b27e Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:49:14 +0000 Subject: [PATCH 07/21] desktop, android: fix sections layout in Lock view and "connect to remote desktop" views (#7163) --- .../common/views/remote/ConnectDesktopView.kt | 14 ++-- .../views/usersettings/PrivacySettings.kt | 70 +++++++++---------- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index 81e1afd22c..86be99d1bf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -148,7 +148,6 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) { AppBarTitle(stringResource(MR.strings.connecting_to_desktop)) SectionView(stringResource(MR.strings.connecting_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { CtrlDeviceNameText(session, rc) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) CtrlDeviceVersionText(session) } @@ -257,7 +256,6 @@ private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessC AppBarTitle(stringResource(MR.strings.verify_connection)) SectionView(stringResource(MR.strings.connected_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { CtrlDeviceNameText(session, rc) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) CtrlDeviceVersionText(session) } @@ -265,16 +263,15 @@ private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessC SectionView(stringResource(MR.strings.verify_code_with_desktop)) { SessionCodeText(sessCode) + SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) { + Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary) + TextIconSpaced(false) + Text(generalGetString(MR.strings.confirm_verb)) + } } SectionDividerSpaced() - SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) { - Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary) - TextIconSpaced(false) - Text(generalGetString(MR.strings.confirm_verb)) - } - SectionView { DisconnectButton(onClick = ::disconnectDesktop) } @@ -312,7 +309,6 @@ private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: AppBarTitle(stringResource(MR.strings.connected_to_desktop)) SectionView(stringResource(MR.strings.connected_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(rc.deviceViewName) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) CtrlDeviceVersionText(session) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index cf34fd5a44..0b698b2c5d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -665,46 +665,46 @@ fun SimplexLockView( } } } - if (performLA.value && laMode.value == LAMode.PASSCODE) { - SectionDividerSpaced() - SectionView(stringResource(MR.strings.self_destruct_passcode)) { - val openInfo = { - ModalManager.start.showModal { - SelfDestructInfoView() - } + } + if (performLA.value && laMode.value == LAMode.PASSCODE) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.self_destruct_passcode)) { + val openInfo = { + ModalManager.start.showModal { + SelfDestructInfoView() } - SettingsActionItemWithContent(null, null, click = openInfo) { - SharedPreferenceToggleWithIcon( - stringResource(MR.strings.enable_self_destruct), - painterResource(MR.images.ic_info), - openInfo, - remember { selfDestructPref.state }.value - ) { - toggleSelfDestruct(selfDestructPref) - } + } + SettingsActionItemWithContent(null, null, click = openInfo) { + SharedPreferenceToggleWithIcon( + stringResource(MR.strings.enable_self_destruct), + painterResource(MR.images.ic_info), + openInfo, + remember { selfDestructPref.state }.value + ) { + toggleSelfDestruct(selfDestructPref) } + } - if (remember { selfDestructPref.state }.value) { - Column(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) { - Text( - stringResource(MR.strings.self_destruct_new_display_name), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) }) - LaunchedEffect(selfDestructDisplayName.value) { - val new = selfDestructDisplayName.value - if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) { - selfDestructDisplayNamePref.set(new) - } + if (remember { selfDestructPref.state }.value) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) { + Text( + stringResource(MR.strings.self_destruct_new_display_name), + fontSize = 16.sp, + modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) + ) + ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) }) + LaunchedEffect(selfDestructDisplayName.value) { + val new = selfDestructDisplayName.value + if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) { + selfDestructDisplayNamePref.set(new) } } - SectionItemView({ changeSelfDestructPassword() }) { - Text( - stringResource(MR.strings.change_self_destruct_passcode), - color = MaterialTheme.colors.primary - ) - } + } + SectionItemView({ changeSelfDestructPassword() }) { + Text( + stringResource(MR.strings.change_self_destruct_passcode), + color = MaterialTheme.colors.primary + ) } } } From b3944af735569476a6daa272248be4b12ac4c566 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:43:31 +0000 Subject: [PATCH 08/21] desktop: fix crash when opening a video full screen (#7167) Opening a video full screen could crash with NoSuchElementException from VLC native-library discovery. Each MediaPlayerFactory() runs a JDK ServiceLoader (not thread-safe), and the second preview factory added in #6924 let the render thread and preview thread construct factories concurrently. Serialize the two constructions behind a shared lock. --- .../common/platform/RecAndPlay.desktop.kt | 9 ++- ...6-29-fix-desktop-video-vlc-factory-race.md | 77 +++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 plans/2026-06-29-fix-desktop-video-vlc-factory-race.md diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index 8d26f2f085..d59677b726 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -13,9 +13,14 @@ import java.io.File import java.util.* import kotlin.math.max -internal val vlcFactory: MediaPlayerFactory by lazy { MediaPlayerFactory() } +// Serialize the two factory constructions: each MediaPlayerFactory() runs VLC native discovery via +// a JDK ServiceLoader, which is not thread-safe. Building both factories concurrently (e.g. vlcFactory +// on the render thread while vlcPreviewFactory is built on the preview thread) corrupts the ServiceLoader +// enumeration and throws NoSuchElementException from CompoundEnumeration.nextElement. +private val vlcFactoryLock = Any() +internal val vlcFactory: MediaPlayerFactory by lazy { synchronized(vlcFactoryLock) { MediaPlayerFactory() } } // No hardware acceleration - more secure for previews -internal val vlcPreviewFactory: MediaPlayerFactory by lazy { MediaPlayerFactory("--avcodec-hw=none") } +internal val vlcPreviewFactory: MediaPlayerFactory by lazy { synchronized(vlcFactoryLock) { MediaPlayerFactory("--avcodec-hw=none") } } actual class RecorderNative: RecorderInterface { private var player: MediaPlayer? = null diff --git a/plans/2026-06-29-fix-desktop-video-vlc-factory-race.md b/plans/2026-06-29-fix-desktop-video-vlc-factory-race.md new file mode 100644 index 0000000000..f75b962a4b --- /dev/null +++ b/plans/2026-06-29-fix-desktop-video-vlc-factory-race.md @@ -0,0 +1,77 @@ +# Fix desktop crash when opening a video (VLC factory init race) + +## Problem (user-facing) + +Opening a video in full screen on desktop can crash the app with: + +``` +java.util.NoSuchElementException + at java.base/java.lang.CompoundEnumeration.nextElement + ... java.util.ServiceLoader ... + at uk.co.caprica.vlcj.factory.discovery.provider.DirectoryProviderDiscoveryStrategy.getSupportedProviders + at uk.co.caprica.vlcj.factory.MediaPlayerFactory. + at chat.simplex.common.platform.RecAndPlay_desktopKt.vlcFactory_delegate$lambda$0(RecAndPlay.desktop.kt:16) +``` + +The crash is intermittent and originates from the lazy initialization of the shared +`MediaPlayerFactory` while a video full-screen view is being composed. + +## Cause + +Each `MediaPlayerFactory()` constructor runs VLC native-library discovery, which iterates a +JDK `ServiceLoader` over `DiscoveryDirectoryProvider`. `ServiceLoader` and the underlying +`CompoundEnumeration` are **not thread-safe**: when two factory constructions run concurrently +on different threads, one enumeration reports `hasNext() == true` and then throws +`NoSuchElementException` from `nextElement()`. + +There are two factories on the desktop: + +- `vlcFactory` — used by the real audio/video players. Its lazy init is triggered on the + AWT/Compose render thread when a video is opened full screen + (`VideoPlayer.initializeMediaPlayerComponent` -> `RecAndPlay.desktop.kt:16`). +- `vlcPreviewFactory` (`--avcodec-hw=none`) — used by preview snapshot helpers, whose lazy init + runs on the dedicated `previewThread` (`VideoPlayer.getOrCreateHelperPlayer`). + +The single-factory invariant established by #6739 ("use shared VLC media-player factory") was +the original protection against concurrent factory construction. #6924 reintroduced a second +factory (`vlcPreviewFactory`) for hardware-acceleration-free previews, reopening the race: the +render thread can construct `vlcFactory` while `previewThread` constructs `vlcPreviewFactory`, +producing two concurrent `ServiceLoader` discoveries and the crash. + +## Fix + +Serialize the two `MediaPlayerFactory()` constructions behind a shared lock so their +native-discovery / `ServiceLoader` runs can never overlap: + +```kotlin +private val vlcFactoryLock = Any() +internal val vlcFactory: MediaPlayerFactory by lazy { synchronized(vlcFactoryLock) { MediaPlayerFactory() } } +internal val vlcPreviewFactory: MediaPlayerFactory by lazy { synchronized(vlcFactoryLock) { MediaPlayerFactory("--avcodec-hw=none") } } +``` + +Both factories are preserved, including the preview factory's `--avcodec-hw=none` option. The +lock guards only the one-time construction of each factory, so there is no steady-state +contention once both are built. + +### Why this approach + +- **Minimal and intent-preserving.** Keeps both factories (preview still needs + `--avcodec-hw=none`) and only adds serialization, restoring the no-concurrent-construction + guarantee that #6739 relied on. +- **Lazy-preserving.** Each factory is still built strictly on demand; the lock only matters in + the rare window where both initialize at the same time. A smaller diff (forcing + `vlcFactory` first inside the preview initializer) was rejected because it would eagerly + construct the main factory whenever a preview is generated and reads as dead code. + +### Known trade-off + +Because `vlcFactory` is initialized on the AWT/render thread, if `previewThread` is mid-construction +of `vlcPreviewFactory` the render thread can briefly block on the lock until native discovery +finishes. This replaces an intermittent crash with a rare, short stall — an acceptable trade. +A more thorough follow-up would either collapse to a single factory (passing `:avcodec-hw=none` +as a per-media option on preview prepare) or eagerly initialize both factories off the render +thread at startup. + +## Scope + +- `apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt` From 1f2a7266afa0ca7328837af4be932843813d85c0 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 29 Jun 2026 21:40:11 +0100 Subject: [PATCH 09/21] readme: github-banner (#7172) * readme: github-banner * update readme --- README.md | 4 +++- images/github-banner.jpg | Bin 0 -> 159335 bytes 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 images/github-banner.jpg diff --git a/README.md b/README.md index 252fc95708..a515a25df6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ | 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) | -SimpleX logo +SimpleX logo + +Invest in SimpleX Chat. [Register now](https://simplexchat.typeform.com/crowdfunding). # SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design! diff --git a/images/github-banner.jpg b/images/github-banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7a0d730b8ec44489976a449f8e6983fe58e467b GIT binary patch literal 159335 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

CJAWKUDfb`4E7rYj@pO@N6V@d<%b< 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%0A0>C|-;{f+3zr+| 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 z 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 D-wq=1 literal 0 HcmV?d00001 From 8714e2e023a0ebf7cdcf7bb8cb062f8e393b33fd Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 29 Jun 2026 21:40:11 +0100 Subject: [PATCH 10/21] readme: github-banner (#7172) * readme: github-banner * update readme --- README.md | 4 +++- images/github-banner.jpg | Bin 0 -> 159335 bytes 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 images/github-banner.jpg diff --git a/README.md b/README.md index 252fc95708..a515a25df6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ | 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) | -SimpleX logo +SimpleX logo + +Invest in SimpleX Chat. [Register now](https://simplexchat.typeform.com/crowdfunding). # SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design! diff --git a/images/github-banner.jpg b/images/github-banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7a0d730b8ec44489976a449f8e6983fe58e467b GIT binary patch literal 159335 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

CJAWKUDfb`4E7rYj@pO@N6V@d<%b< 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%0A0>C|-;{f+3zr+| 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 z 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 D-wq=1 literal 0 HcmV?d00001 From 2f10e057e22734389a6e3ea035c07d70c455ce77 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 1 Jul 2026 13:42:26 +0100 Subject: [PATCH 11/21] feat(directory): harden group member admission (#7180) * feat(directory): harden group member admission Default new registered groups to requiring a captcha and advise owners of recommended public-group settings at registration submission. Add CLI flags to override per-group admission policy directory-wide: - --prohibited-to-observer: delete a member's message and demote them to observer when they post content prohibited by the group's settings - --always-captcha: require a captcha from joining members in all groups - --knocking: require admin review before admitting members in all groups * fix(directory): reword recommended settings notice Address review: replace the enumerated settings list with a concise recommendation and reference the group's filter command by id. --------- Co-authored-by: shum --- .../src/Directory/Events.hs | 8 +- .../src/Directory/Options.hs | 21 ++ .../src/Directory/Service.hs | 46 +++- .../src/Directory/Store.hs | 11 + tests/Bots/DirectoryTests.hs | 238 +++++++++++------- 5 files changed, 226 insertions(+), 98 deletions(-) diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index bfbc025a49..3bff611a28 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -27,7 +27,7 @@ import qualified Data.Attoparsec.Text as A import Data.Char (isSpace) import Data.Either (fromRight) import Data.Functor (($>)) -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) @@ -38,6 +38,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink, MsgContent (..)) import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences (GroupFeature) import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (AgentErrorType (..)) import Simplex.Messaging.Encoding.String @@ -52,6 +53,7 @@ data DirectoryEvent | DEGroupLinkCheck GroupInfo | DEPendingMember GroupInfo GroupMember | DEPendingMemberMsg GroupInfo GroupMember ChatItemId Text + | DEGroupItemProhibited GroupInfo GroupMember ChatItemId GroupFeature -- a member posted content prohibited by the group's settings | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed | DEServiceRoleChanged GroupInfo GroupMemberRole | DEContactRemovedFromGroup ContactId GroupInfo @@ -84,8 +86,10 @@ crDirectoryEvent_ = \case CEvtJoinedGroupMember {groupInfo, member = m} | pending m -> Just $ DEPendingMember groupInfo m | otherwise -> Nothing - CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g _scopeInfo) ci : _} -> case ci of + CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g scopeInfo) ci : _} -> case ci of ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t + -- only moderate prohibited content in the main group, not in member-support/onboarding scope + ChatItem {chatDir = CIGroupRcv m, content = CIRcvGroupFeatureRejected gf} | isNothing scopeInfo -> Just $ DEGroupItemProhibited g m (chatItemId' ci) gf _ -> Nothing CEvtMemberRole {groupInfo, member, toRole} | groupMemberId' member == groupMemberId' (membership groupInfo) -> Just $ DEServiceRoleChanged groupInfo toRole diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 5d51023781..23115ec7c7 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -44,6 +44,9 @@ data DirectoryOpts = DirectoryOpts searchResults :: Int, webFolder :: Maybe FilePath, linkCheckInterval :: Int, + prohibitedToObserver :: Bool, + alwaysCaptcha :: Bool, + knocking :: Bool, testing :: Bool } @@ -177,6 +180,21 @@ directoryOpts appDir defaultDbName = do <> help "Interval in seconds to check public group link data (default: 1800)" <> value 1800 ) + prohibitedToObserver <- + switch + ( long "prohibited-to-observer" + <> help "Set a member to observer (and delete the message) when they post content prohibited by the group's settings" + ) + alwaysCaptcha <- + switch + ( long "always-captcha" + <> help "Require a captcha from joining members in all groups, regardless of per-group filter settings" + ) + knocking <- + switch + ( long "knocking" + <> help "Require admin review (knocking) before joining members are admitted in all groups, regardless of group preference" + ) pure DirectoryOpts { coreOptions, @@ -199,6 +217,9 @@ directoryOpts appDir defaultDbName = do searchResults = 10, webFolder, linkCheckInterval, + prohibitedToObserver, + alwaysCaptcha, + knocking, testing = False } diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 63e1a0ff69..bedcb87da3 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -271,7 +271,7 @@ directoryService st opts cfg = do acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)) acceptMemberHook - DirectoryOpts {profileNameLimit} + DirectoryOpts {profileNameLimit, alwaysCaptcha, knocking} ServiceState {blockedWordsCfg} g GroupLinkInfo {memberRole} @@ -280,7 +280,8 @@ acceptMemberHook when (useMemberFilter img $ rejectNames a) checkName pure $ if - | useMemberFilter img (passCaptcha a) -> (GAPendingApproval, GRMember) + | knocking -> (GAPendingReview, memberRole) + | alwaysCaptcha || useMemberFilter img (passCaptcha a) -> (GAPendingApproval, GRMember) | useMemberFilter img (makeObserver a) -> (GAAccepted, GRObserver) | otherwise -> (GAAccepted, memberRole) where @@ -294,6 +295,11 @@ acceptMemberHook groupMemberAcceptance :: GroupInfo -> DirectoryMemberAcceptance groupMemberAcceptance GroupInfo {customData} = (\DirectoryGroupData {memberAcceptance = ma} -> ma) $ fromCustomData customData +recommendedSettingsNotice :: UserGroupRegId -> Text +recommendedSettingsNotice userGroupId = + "We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them.\n\ + \Captcha verification is enabled. Use /'filter " <> tshow userGroupId <> "' to change it." + useMemberFilter :: Maybe ImageData -> Maybe ProfileCondition -> Bool useMemberFilter img_ = \case Just PCAll -> True @@ -311,7 +317,7 @@ readBlockedWordsConfig DirectoryOpts {blockedFragmentsFile, blockedWordsFile, na pure BlockedWordsConfig {blockedFragments, blockedWords, extensionRules, spelling} directoryServiceEvent :: DirectoryLog -> DirectoryOpts -> ServiceState -> User -> ChatController -> DirectoryEvent -> IO () -directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc = \case +directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults, prohibitedToObserver, alwaysCaptcha} env@ServiceState {searchRequests} user@User {userId} cc = \case DEContactConnected ct -> deContactConnected ct DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner @@ -319,6 +325,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName DEGroupLinkCheck g -> deGroupLinkCheck g DEPendingMember g m -> dePendingMember g m DEPendingMemberMsg g m ciId t -> dePendingMemberMsg g m ciId t + DEGroupItemProhibited g m ciId gf -> when prohibitedToObserver $ deGroupItemProhibited g m ciId gf DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role DEServiceRoleChanged g role -> deServiceRoleChanged g role DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g @@ -404,7 +411,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName processInvitation :: Contact -> GroupInfo -> Maybe GroupReg -> IO () processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = \case - Nothing -> addGroupReg notifyAdminUsers st cc ct g GRSProposed joinGroup + Nothing -> addGroupReg notifyAdminUsers st cc user ct g GRSProposed joinGroup Just _gr -> setGroupStatus notifyAdminUsers st env cc groupId GRSProposed joinGroup where joinGroup _ = do @@ -436,7 +443,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Left e -> sendMessage cc ct $ "Error: getDuplicateGroup. Please notify the developers.\n" <> T.pack e where askConfirmation = - addGroupReg notifyAdminUsers st cc ct g GRSPendingConfirmation $ \GroupReg {userGroupRegId} -> do + addGroupReg notifyAdminUsers st cc user ct g GRSPendingConfirmation $ \GroupReg {userGroupRegId} -> do sendMessage cc ct $ "The group " <> groupNameDescr p <> " is already submitted to the directory.\nTo confirm the registration, please send:" sendMessage cc ct $ "/confirm " <> tshow userGroupRegId <> ":" <> viewName displayName @@ -488,6 +495,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName \Please add it to the group welcome message.\n\ \For example, add:" notifyOwner gr' $ "Link to join the group " <> displayName <> ": " <> groupLinkText gLink + notifyOwner gr' $ recommendedSettingsNotice (userGroupRegId gr') Left (ChatError e) -> case e of CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." @@ -650,6 +658,19 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." <> if canSendVoiceCaptcha g m then "\nSend /audio to receive a voice captcha." else "" + -- gated by --prohibited-to-observer at the dispatch above + deGroupItemProhibited :: GroupInfo -> GroupMember -> ChatItemId -> GroupFeature -> IO () + deGroupItemProhibited GroupInfo {groupId} m@GroupMember {memberRole} ciId gf = + when (memberRole == GRMember) $ do + let gmId = groupMemberId' m + logInfo $ "Member " <> tshow gmId <> " posted prohibited content (" <> tshow gf <> ") in group " <> tshow groupId <> "; deleting and setting to observer" + sendChatCmd cc (APIDeleteMemberChatItem groupId [ciId]) >>= \case + Right CRChatItemsDeleted {} -> pure () + r -> logError $ "deGroupItemProhibited: unexpected delete response: " <> tshow r + sendChatCmd cc (APIMembersRole groupId [gmId] GRObserver) >>= \case + Right CRMembersRoleUser {} -> pure () -- empty members = already observer (idempotent), still success + r -> logError $ "deGroupItemProhibited: unexpected set observer response: " <> tshow r + sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> CaptchaMode -> IO () sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts mode = do s <- getCaptchaStr captchaLength "" @@ -776,7 +797,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName memberRequiresCaptcha :: DirectoryMemberAcceptance -> GroupMember -> Bool memberRequiresCaptcha a GroupMember {memberProfile = LocalProfile {image}} = - useMemberFilter image $ passCaptcha a + alwaysCaptcha || useMemberFilter image (passCaptcha a) sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () sendToApprove GroupInfo {groupId, groupProfile = p@GroupProfile {displayName, image = image', publicGroup = pg_}, groupSummary} GroupReg {dbContactId, promoted} gaId = do @@ -982,7 +1003,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName sendChatCmd cc (APIPrepareGroup userId ccLink False groupSLinkData) >>= \case Right (CRNewPreparedChat _ (AChat SCTGroup (Chat (GroupChat gInfo _) _ _))) -> do let gId = groupId' gInfo - addGroupReg notifyAdminUsers st cc ct gInfo GRSProposed $ \_ -> pure () + addGroupReg notifyAdminUsers st cc user ct gInfo GRSProposed $ \_ -> pure () sendChatCmd cc (APIConnectPreparedGroup gId False (Just ownerContact) Nothing) >>= \case Right CRStartedConnectionToGroup {groupInfo = gInfo'} -> withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (storeCxt cc) user gInfo' mId) >>= \case @@ -1007,7 +1028,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName | contactId' ct `isOwner` gr -> sameOwnerReregistration gr gt | otherwise -> sendMessage cc ct $ "This " <> gt <> " is registered by another owner." Left _ -> - addGroupReg notifyAdminUsers st cc ct g (GRSPendingApproval 1) $ \gr -> do + addGroupReg notifyAdminUsers st cc user ct g (GRSPendingApproval 1) $ \gr -> do void $ setGroupRegOwner cc groupId ownerMember sendToApprove g gr 1 | role < GROwner -> sendMessage cc ct $ "You must be the " <> gt <> " owner to register it." @@ -1045,6 +1066,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName in if role >= GROwner then setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval 1) $ \gr' -> do notifyOwner gr' $ "Joined the " <> gt <> " " <> displayName <> ". Registration is pending approval — it may take up to 48 hours." + notifyOwner gr' $ recommendedSettingsNotice (userGroupRegId gr') sendToApprove g gr' 1 else do setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \_ -> pure () @@ -1484,12 +1506,16 @@ setGroupStatusPromo sendReply st env cc GroupReg {dbGroupId = gId} grStatus' grP logGUpdatePromotion st gId grPromoted' continue -addGroupReg :: (Text -> IO ()) -> DirectoryLog -> ChatController -> Contact -> GroupInfo -> GroupRegStatus -> (GroupReg -> IO ()) -> IO () -addGroupReg sendMsg st cc ct g@GroupInfo {groupId} grStatus continue = +addGroupReg :: (Text -> IO ()) -> DirectoryLog -> ChatController -> User -> Contact -> GroupInfo -> GroupRegStatus -> (GroupReg -> IO ()) -> IO () +addGroupReg sendMsg st cc user ct g@GroupInfo {groupId} grStatus continue = addGroupRegStore cc ct g grStatus >>= \case Left e -> sendMsg $ "Error creating group registation for group " <> tshow groupId <> ": " <> T.pack e Right gr -> do logGCreate st gr + let d = toCustomData $ DirectoryGroupData newGroupJoinFilter + withDB' "setGroupCustomData" cc (\db -> setGroupCustomData db user g $ Just d) >>= \case + Right () -> pure () + Left e -> sendMsg $ "Error setting default captcha for group " <> tshow groupId <> ": " <> T.pack e continue gr setGroupStatus :: (Text -> IO ()) -> DirectoryLog -> ServiceState -> ChatController -> GroupId -> GroupRegStatus -> (GroupReg -> IO ()) -> IO () diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index 89c5178f7d..a9a9788e0f 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -52,6 +52,7 @@ module Directory.Store basicJoinFilter, moderateJoinFilter, strongJoinFilter, + newGroupJoinFilter, groupDBError, logGCreate, logGDelete, @@ -164,6 +165,16 @@ strongJoinFilter = makeObserver = Nothing } +-- Default applied to newly registered groups: a captcha challenge is required +-- from every joining member unless the owner changes it with /filter. +newGroupJoinFilter :: DirectoryMemberAcceptance +newGroupJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Nothing, + passCaptcha = Just PCAll, + makeObserver = Nothing + } + type UserGroupRegId = Int64 type GroupApprovalId = Int64 diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index cd6d549581..438a3f9802 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -74,6 +74,9 @@ directoryServiceTests = do describe "list and promote groups" $ do it "should list and promote user's groups" $ testListUserGroups True describe "member admission" $ do + it "should require captcha by default for new groups" testCaptchaByDefault + it "should require captcha in all groups with --always-captcha" testAlwaysCaptcha + it "should require admin review in all groups with --knocking" testKnocking it "should ask member to pass captcha screen" testCapthaScreening it "should send voice captcha on /audio command" testVoiceCaptchaScreening it "should retry with voice captcha after switching to audio mode" testVoiceCaptchaRetry @@ -133,6 +136,9 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = searchResults = 3, webFolder, linkCheckInterval = 0, + prohibitedToObserver = False, + alwaysCaptcha = False, + knocking = False, testing = True } @@ -169,6 +175,8 @@ testDirectoryService ps = bob <## "Please add it to the group welcome message." bob <## "For example, add:" welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." -- putStrLn "*** update profile without link" updateGroupProfile bob "Welcome!" bob <# "'SimpleX Directory'> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message." @@ -396,6 +404,14 @@ testSetRole ps = cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath + cath #> ("#privacy (support) " <> captcha) + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://localhost/g#" cath <## "#privacy: member bob (Bob) is connected" @@ -428,12 +444,18 @@ testJoinGroup ps = cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy (support) 'SimpleX Directory_1'> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory_1'> " . dropTime <$> getTermLine cath + cath #> ("#privacy (support) " <> captcha) + cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" + cath <## "use @'SimpleX Directory' to send messages" + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" - cath - <### [ "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'", - "use @'SimpleX Directory' to send messages", - Predicate (\l -> l == welcomeMsg || dropTime_ l == Just ("#privacy 'SimpleX Directory'> " <> welcomeMsg) || dropTime_ l == Just ("#privacy 'SimpleX Directory_1'> " <> welcomeMsg)) - ] + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" @@ -788,7 +810,7 @@ testNotSentApprovalBadRoles ps = bob `connectVia` dsLink cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 bob ##> "/mr privacy 'SimpleX Directory' member" bob <## "#privacy: you changed the role of 'SimpleX Directory' to member" updateProfileWithLink bob "privacy" welcomeWithLink 1 @@ -811,7 +833,7 @@ testNotApprovedBadRoles ps = bob `connectVia` dsLink cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 updateProfileWithLink bob "privacy" welcomeWithLink 1 notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 bob ##> "/mr privacy 'SimpleX Directory' member" @@ -1019,14 +1041,14 @@ testDuplicateAskConfirmation ps = withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" - _ <- groupAccepted bob "privacy" + _ <- groupAccepted bob "privacy" 1 cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" - welcomeWithLink <- groupAccepted cath "privacy" + welcomeWithLink <- groupAccepted cath "privacy" 1 groupNotFound bob "privacy" completeRegistrationId superUser cath "privacy" "Privacy" welcomeWithLink 2 1 groupFound bob "privacy" @@ -1050,7 +1072,7 @@ testDuplicateProhibitConfirmation ps = withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." @@ -1069,14 +1091,14 @@ testDuplicateProhibitWhenUpdated ps = withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" - welcomeWithLink' <- groupAccepted cath "privacy" + welcomeWithLink' <- groupAccepted cath "privacy" 1 groupNotFound cath "privacy" completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 groupFound cath "privacy" @@ -1100,14 +1122,14 @@ testDuplicateProhibitApproval ps = withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" - welcomeWithLink' <- groupAccepted cath "privacy" + welcomeWithLink' <- groupAccepted cath "privacy" 1 updateProfileWithLink cath "privacy" welcomeWithLink' 1 notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2 groupNotFound cath "privacy" @@ -1194,6 +1216,100 @@ checkListings listed promoted = do map groupName gs `shouldBe` expected groupName DirectoryEntry {displayName} = displayName +testAlwaysCaptcha :: HasCallStack => TestParams -> IO () +testAlwaysCaptcha ps = + withDirectoryServiceOpts ps (\o -> o {alwaysCaptcha = True}) $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + -- disable the per-group captcha filter; --always-captcha must still force it + bob #> "@'SimpleX Directory' /filter 1 off" + bob <# "'SimpleX Directory'> > /filter 1 off" + bob <## " Spam filter settings for group privacy set to:" + bob <## "- reject long/inappropriate names: disabled" + bob <## "- pass captcha to join: disabled" + bob <## "" + bob <## "/'filter 1 name' - enable name filter" + bob <## "/'filter 1 captcha' - enable captcha challenge" + bob <## "/'filter 1 name captcha' - enable both" + bob #> "@'SimpleX Directory' /role 1" + bob <# "'SimpleX Directory'> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /'role 1 observer' to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath + cath #> ("#privacy (support) " <> captcha) + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" + cath <## "#privacy: you joined the group" + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath is connected" + +testKnocking :: HasCallStack => TestParams -> IO () +testKnocking ps = + withDirectoryServiceOpts ps (\o -> o {knocking = True}) $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + bob #> "@'SimpleX Directory' /role 1" + bob <# "'SimpleX Directory'> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /'role 1 observer' to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, connecting to group moderators for admission to group" + cath <## "#privacy: 'SimpleX Directory' accepted you to the group, pending review" + bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting and pending review...), use /_accept member #1 3 to accept member" + +testCaptchaByDefault :: HasCallStack => TestParams -> IO () +testCaptchaByDefault ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + -- the owner never ran /filter; captcha is on by default for new groups + bob #> "@'SimpleX Directory' /role 1" + bob <# "'SimpleX Directory'> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /'role 1 observer' to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath + cath #> ("#privacy (support) " <> captcha) + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" + cath <## "#privacy: you joined the group" + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath is connected" + testCapthaScreening :: HasCallStack => TestParams -> IO () testCapthaScreening ps = withDirectoryService ps $ \superUser dsLink -> @@ -1209,16 +1325,6 @@ testCapthaScreening ps = bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - -- enable captcha - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- connect with captcha screen _ <- join cath groupLink cath #> "#privacy (support) 123" -- sending incorrect captcha @@ -1307,16 +1413,6 @@ testVoiceCaptchaScreening ps@TestParams {tmpPath} = do bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - -- enable captcha - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- cath joins, receives text captcha with /audio hint cath ##> ("/c " <> groupLink) cath <## "connection request sent!" @@ -1376,15 +1472,6 @@ testVoiceCaptchaRetry ps@TestParams {tmpPath} = do bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- cath joins, receives text captcha with /audio hint cath ##> ("/c " <> groupLink) cath <## "connection request sent!" @@ -1437,15 +1524,6 @@ testVoiceCaptchaVoiceDisabled ps@TestParams {tmpPath} = do bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- disable voice messages in the group bob ##> "/set voice #privacy off" bob <## "updated group preferences:" @@ -1504,15 +1582,6 @@ testVoiceCaptchaOldClient ps@TestParams {tmpPath} = do bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- disable voice messages in the group bob ##> "/set voice #privacy off" bob <## "updated group preferences:" @@ -1543,20 +1612,24 @@ testVoiceCaptchaOldClient ps@TestParams {tmpPath} = do cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" -withDirectoryServiceVoiceCaptcha :: HasCallStack => TestParams -> FilePath -> (TestCC -> String -> IO ()) -> IO () -withDirectoryServiceVoiceCaptcha ps voiceScript test = do +withDirectoryServiceOpts :: HasCallStack => TestParams -> (DirectoryOpts -> DirectoryOpts) -> (TestCC -> String -> IO ()) -> IO () +withDirectoryServiceOpts ps modOpts test = do dsLink <- withNewTestChatCfg ps testCfg serviceDbPrefix directoryProfile $ \ds -> withNewTestChatCfg ps testCfg "super_user" aliceProfile $ \superUser -> do connectUsers ds superUser ds ##> "/ad" getContactLink ds True - let opts = (mkDirectoryOpts ps [KnownContact 2 "alice"] Nothing Nothing) {voiceCaptchaGenerator = Just voiceScript} + let opts = modOpts $ mkDirectoryOpts ps [KnownContact 2 "alice"] Nothing Nothing runDirectory testCfg opts $ withTestChatCfg ps testCfg "super_user" $ \superUser -> do superUser <## "subscribed 1 connections on server localhost" test superUser dsLink +withDirectoryServiceVoiceCaptcha :: HasCallStack => TestParams -> FilePath -> (TestCC -> String -> IO ()) -> IO () +withDirectoryServiceVoiceCaptcha ps voiceScript = + withDirectoryServiceOpts ps (\o -> o {voiceCaptchaGenerator = Just voiceScript}) + testRestoreDirectory :: HasCallStack => TestParams -> IO () testRestoreDirectory ps = do testListUserGroups False ps @@ -1729,7 +1802,7 @@ registerGroup su u n fn = registerGroupId su u n fn 1 1 registerGroupId :: TestCC -> TestCC -> String -> String -> Int -> Int -> IO () registerGroupId su u n fn gId ugId = do submitGroup u n fn - welcomeWithLink <- groupAccepted u n + welcomeWithLink <- groupAccepted u n ugId completeRegistrationId su u n fn welcomeWithLink gId ugId submitGroup :: TestCC -> String -> String -> IO () @@ -1740,8 +1813,8 @@ submitGroup u n fn = do u ##> ("/a " <> viewName n <> " 'SimpleX Directory' admin") u <## ("invitation to join the group #" <> viewName n <> " sent to 'SimpleX Directory'") -groupAccepted :: TestCC -> String -> IO String -groupAccepted u n = do +groupAccepted :: TestCC -> String -> Int -> IO String +groupAccepted u n ugId = do u <### [ WithTime ("'SimpleX Directory'> Joining the group " <> n <> "…"), ConsoleString ("#" <> viewName n <> ": 'SimpleX Directory' joined the group") @@ -1751,7 +1824,10 @@ groupAccepted u n = do u <## "" u <## "Please add it to the group welcome message." u <## "For example, add:" - dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u -- welcome message with link + welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u + u <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + u <## ("Captcha verification is enabled. Use /'filter " <> show ugId <> "' to change it.") + pure welcomeWithLink completeRegistration :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () completeRegistration su u n fn welcomeWithLink gId = @@ -1885,15 +1961,6 @@ testCaptchaTooManyAttempts ps = bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." @@ -1932,15 +1999,6 @@ testCaptchaUnknownCommand ps = bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." @@ -2003,6 +2061,8 @@ testRegisterChannelViaCard ps = ] -- owner sends a message to trigger member introduction bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" superUser <## "news" superUser <##. "Link to join channel: " @@ -2101,6 +2161,8 @@ testDeleteChannelRegistration ps = bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" ] bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" superUser <## "news" superUser <##. "Link to join channel: " @@ -2145,6 +2207,8 @@ testReregistrationAlreadyListed ps = bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" ] bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" superUser <## "news" superUser <##. "Link to join channel: " @@ -2204,6 +2268,8 @@ testLinkCheckUpdatesCount ps = do bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" ] bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" superUser <## "news" superUser <##. "Link to join channel: " From 10958d447dc4592e5a1f3394f26da9ccf8159c0e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 2 Jul 2026 07:46:12 +0100 Subject: [PATCH 12/21] ios: restore resolved packages --- .../xcshareddata/swiftpm/Package.resolved | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..978e1d9630 --- /dev/null +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,66 @@ +{ + "originHash" : "60aeecb7917535a5e44ade0dbb5411ab112a959283e565a04c212c8af4e7dee9", + "pins" : [ + { + "identity" : "codescanner", + "kind" : "remoteSourceControl", + "location" : "https://github.com/twostraws/CodeScanner", + "state" : { + "revision" : "34da57fb63b47add20de8a85da58191523ccce57", + "version" : "2.5.0" + } + }, + { + "identity" : "elegant-emoji-picker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Finalet/Elegant-Emoji-Picker", + "state" : { + "branch" : "main", + "revision" : "71d2d46092b4d550cc593614efc06438f845f6e6" + } + }, + { + "identity" : "ink", + "kind" : "remoteSourceControl", + "location" : "https://github.com/johnsundell/ink", + "state" : { + "revision" : "bcc9f219900a62c4210e6db726035d7f03ae757b", + "version" : "0.6.0" + } + }, + { + "identity" : "lzstring-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Ibrahimhass/lzstring-swift", + "state" : { + "revision" : "7f62f21de5b18582a950e1753b775cc614722407" + } + }, + { + "identity" : "swiftygif", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kirualex/SwiftyGif", + "state" : { + "revision" : "5e8619335d394901379c9add5c4c1c2f420b3800" + } + }, + { + "identity" : "webrtc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/simplex-chat/WebRTC.git", + "state" : { + "revision" : "34bedc50f9c58dccf4967ea59c7e6a47d620803b" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", + "version" : "5.1.2" + } + } + ], + "version" : 3 +} From 2c2337b07c41e5ad6c93c434ca2093c41f963d3d Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:48:51 +0000 Subject: [PATCH 13/21] android, desktop: keep wide images at natural aspect ratio without crashing (#7125) * android, desktop: keep wide images at natural aspect ratio without crashing The merged fix clamped the framed image preview's aspect ratio at 2.33, which prevents the Constraints overflow crash but reshapes every image wider than 2.33:1 to 2.33:1. Compute the box height directly (height = w * min(h / w, 2.33f)) instead of deriving it via Modifier.aspectRatio. Very wide images keep their natural ratio (no upper clamp) while taller images stay capped at 2.33, and the overflow-prone width = height * ratio derivation is removed entirely. This mirrors how the iOS app sizes image previews. * docs: update wide-image crash plan for natural-ratio fix * remove comment --------- Co-authored-by: Evgeny --- .../common/views/chat/item/CIImageView.kt | 2 +- plans/2026-06-23-wide-image-crash.md | 82 +++++++++++-------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 65dbe7cb02..ef561fc3e0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -176,7 +176,7 @@ fun CIImageView( .then( if (!smallView) { val w = if (previewBitmap.width * 0.97 <= previewBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH - Modifier.width(w).aspectRatio((previewBitmap.width.toFloat() / previewBitmap.height.toFloat()).coerceIn(1f / 2.33f, 2.33f)) + Modifier.width(w).height(w * (previewBitmap.height.toFloat() / previewBitmap.width.toFloat()).coerceAtMost(2.33f)) } else Modifier ) .desktopModifyBlurredState(!smallView, blurred, showMenu), diff --git a/plans/2026-06-23-wide-image-crash.md b/plans/2026-06-23-wide-image-crash.md index effe8695b6..b409befda2 100644 --- a/plans/2026-06-23-wide-image-crash.md +++ b/plans/2026-06-23-wide-image-crash.md @@ -47,52 +47,62 @@ and reaches the unbounded `aspectRatio`. ## Fix -Add the symmetric **upper** bound to the clamp, mirroring the existing lower -bound and the tall-image height cap already enforced in `PriorityLayout` -(`FramedItemView.kt`: `maxImageHeight = constraints.maxWidth * 2.33f`): +An initial fix (already merged) added a symmetric upper bound to the ratio, +`coerceIn(1f / 2.33f, 2.33f)`. That stops the crash but reshapes every image +wider than 2.33:1 — including legitimate panoramas — to 2.33:1. This change +**supersedes** it: stop routing the box height through `aspectRatio` and compute +it **directly**, so the dangerous `width = height × ratio` derivation never +happens. The wide side is then left at its **natural ratio** (no upper clamp); +only the tall side is capped at `2.33`, exactly as before. This mirrors what the +iOS app already does (`height = w × heightRatio`, `heightRatio = min(h / w, 2.33)`): ```kotlin -// after +// before (merged interim fix) Modifier.width(w).aspectRatio((previewBitmap.width.toFloat() / previewBitmap.height.toFloat()).coerceIn(1f / 2.33f, 2.33f)) +// after +Modifier.width(w).height(w * (previewBitmap.height.toFloat() / previewBitmap.width.toFloat()).coerceAtMost(2.33f)) ``` -This is the minimal one-line change. With the cap, the box width is pinned -(`≤ DEFAULT_MAX_IMAGE_WIDTH = 500.dp`) and the height-driven width becomes -`height × 2.33 ≈ 2714 px` — three orders of magnitude inside the 262142 limit — -so the measurement can never overflow at any screen density. `2.33` is the -project's single "most-extreme allowed image proportion" constant: an image is -now clamped to at most 2.33:1 in **either** direction, the same rule already -applied to tall images. +With this form the box width is pinned (`≤ DEFAULT_MAX_IMAGE_WIDTH = 500.dp`) and +the height is `w × min(h / w, 2.33)`, which is always in `(0, w × 2.33]` — at most +`≈ 1165.dp`. Both dimensions are therefore always far inside the 262142 px +`Constraints` limit **at any aspect ratio and any screen density**, so the crash +is structurally impossible rather than merely bounded. A very wide image renders +at its true proportions (a thin strip) instead of being reshaped to 2.33:1. -## Why 2.33 (and not a larger cap) +This does not disturb the framed item's text-width adaptation: that uses +`.width(IntrinsicSize.Max)` (`FramedItemView.kt`), which reads the image box's +intrinsic **width** — still pinned to `w` — so only the wide side's height +derivation changes. -`2.33` is provably safe by construction because it ties the wide bound to the -same proportion the layout already guarantees for height, independent of density. -A larger cap (e.g. 50–200) would preserve the natural shape of genuine panoramas -but relies on an assumption about the maximum measured height and loses the -symmetry with the tall-image rule. The trade-off accepted here is that wide -images between 2.33:1 and the crash threshold now display at 2.33:1 (the very -wide remainder shown as a thin strip with `ContentScale.FillWidth`) rather than -at their natural ratio — a cosmetic change in exchange for a guaranteed-safe, -consistent fix. +## Why compute height directly (vs clamping the ratio) + +The earlier candidate fix clamped the ratio (`coerceIn(1f / 2.33f, 2.33f)`). That +is safe, but it reshapes every image wider than 2.33:1 — including legitimate +panoramas — to 2.33:1, because `ContentScale.FillWidth` cannot fill the +over-tall box and the image letterboxes. Computing the height directly removes +the overflow-prone code path entirely *and* preserves the natural shape of wide +images, so there is no display trade-off to accept. It also makes the Compose and +iOS image-sizing logic parallel. ## Scope / non-goals -- Only the `!smallView` framed Box uses a media-derived `aspectRatio`; it is now - clamped. The chat-list `smallView` preview is locked to a fixed `36.sp` square, - and all other image/video/link paths size with `.width(...)` + `ContentScale` - (no `aspectRatio`), so none of them can hit this overflow. -- Two follow-ups were identified but intentionally left out to keep the diff - minimal: (1) a **symmetric wide guard** in the bitmap decoders - (`outWidth > outHeight * 256`) for defense-in-depth across all consumers, and - (2) extracting the duplicated `2.33` literal (now in `CIImageView.kt` and - `FramedItemView.kt`) into a shared `MAX_IMAGE_ASPECT_RATIO` constant. +- Only the `!smallView` framed Box derived its size from the image ratio; it now + computes height directly. The chat-list `smallView` preview is locked to a fixed + `36.sp` square, and all other image/video/link paths size with `.width(...)` + + `ContentScale` (no `aspectRatio`), so none of them can hit this overflow. +- One follow-up was identified but intentionally left out to keep the diff + minimal: a **symmetric wide guard** in the bitmap decoders + (`outWidth > outHeight * 256`, mirroring the existing tall guard in + `Images.android.kt` / `Images.desktop.kt`) for defense-in-depth, so any future + consumer rendering a decoded bitmap is protected at the source. ## iOS -iOS is **not** affected by the crash. It carries the same lopsided logic — -`heightRatio` (`apps/ios/SimpleXChat/ImageUtils.swift`) caps only the tall side -(`min(size.height / size.width, 2.33)`) — but SwiftUI lays out with `CGFloat` -frames and has no `Constraints` packing limit, so a 4000×1 image yields a valid -(sub-pixel height) frame instead of throwing. No iOS change is required for the -crash; bounding the wide side there would only be a cosmetic parity tweak. +iOS is **not** affected by the crash, and the fix above brings the two platforms +into alignment. iOS already sizes the preview by computing the height directly — +`height = w × heightRatio`, `heightRatio = min(size.height / size.width, 2.33)` +(`apps/ios/SimpleXChat/ImageUtils.swift`) — which is exactly the form now used on +Android/desktop. (Even before, SwiftUI lays out with `CGFloat` frames and has no +`Constraints` packing limit, so a 4000×1 image yielded a valid sub-pixel-height +frame rather than throwing.) No iOS change is required. From 10a814694cc0da03bb065f14b3e4c3a7ff3e36f3 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:51:52 +0400 Subject: [PATCH 14/21] core, ui: support SimpleX names (#7045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deps: bump simplexmq for ConnectTarget * chat: migration adds simplex_name to contacts, groups, connections Nullable TEXT column on all three tables, with partial indexes on contacts(user_id, simplex_name) and groups(user_id, simplex_name) for the upcoming connectPlanName lookup. connections.simplex_name is the transient carrier from APIConnect -> XInfo handler, where the value is copied to contacts.simplex_name at delayed create. No reads or writes yet - column threading lands in subsequent commits. * tests: provide namesConfig = Nothing in smpServerCfg Follow-up to the simplexmq pin bump (ee0a45e9). The new namesConfig :: Maybe NamesConfig field on ServerConfig (introduced in simplexmq's namespace branch) needs to appear in the test fixture's record literal, otherwise the test suite fails to compile under -Werror. Disabled by default (Nothing). * chat: thread simplexName through Contact/GroupInfo/Connection records Adds `simplexName :: Maybe SimplexNameInfo` to the three records and extends every SELECT path that reconstructs them to read the new column. Decoded via eitherToMaybe . strDecode . encodeUtf8 (the codebase's established pattern for Maybe Text -> typed-decode fields), extracted as decodeSimplexName helper since the chain appears in toContact / toContact' / toGroupInfo / toConnection. INSERT paths still write Nothing - the write-side wiring lands in the next commit (Task 7). * deps: bump simplexmq for SimplexNameInfo FromField/ToField * chat: cross-reference groupInfoQueryFields and getGroupAndMember_ Two helpers redundantly maintain the same g.* column list. A future g.* addition must be applied to both sites; the cross-reference comments flag this for maintainers. A proper refactor (reusing groupInfoQueryFields from Connections.hs's inline SELECT) is out of scope for this branch. * chat: persist simplexName on prepare and connect-via-plan write paths createPreparedContact/createPreparedGroup gain a Maybe SimplexNameInfo parameter that they write to contacts.simplex_name / groups.simplex_name directly. createConnection_ writes to connections.simplex_name as a transient carrier for the connect-via-plan path. The XInfo handler in Library/Subscriber.hs reads the connection's simplexName and passes it to createDirectContact so the final contact row captures the name. All current callers pass Nothing; the actual flow lights up when APIConnectPlan accepts ConnectTarget and connectPlanName threads the name through (later commits in this branch). Uses the upstream ToField SimplexNameInfo (simplexmq 0b334b66) for writes; reads continue to go via the soft-degradation helper. * chat: APIConnectPlan accepts ConnectTarget; connectPlanName looks up by name APIConnectPlan/Connect flip from Maybe AConnectionLink to Maybe ConnectTarget. connectPlan dispatches CTLink -> connectPlanLink (the prior body, renamed) and CTName -> connectPlanName (new) which looks the name up against contacts.simplex_name and groups.simplex_name via the new getContactBySimplexName / getGroupInfoBySimplexName store helpers. The hit path returns the contact's / group's stored conn link from preparedContact / preparedGroup; missing prepared state or unknown names return CEInvalidConnReq. RSLV on-chain resolution is out of scope for this branch -- known-name lookup is enough for conversation display, search, and external-link share. connLinkP_ parser is unchanged: APIConnect's preparedLink_ stays ACreatedConnLink-shaped, and the Connect / APIConnectPlan parsers already use inline strP for ConnectTarget without going through the helper. Directory.Service call sites updated to wrap their AConnectionLink in CTLink when invoking APIConnectPlan. * chat: surface simplexName in conversation view + JSON output viewConnectionPlan now shows the simplex name beneath known contacts/groups (ILPKnown, CAPKnown, CAPContactViaAddress, GLPKnown active/prepared/deleted). The TH-derived Contact / GroupInfo JSON instances automatically expose `simplexName` (omitted when Nothing per defaultJSON's omitNothingFields), which unblocks client-side display and search. No JSON test added: there is no Contact-level JSON test module in this codebase; coverage is already provided by defaultJSON's omitNothingFields = True behaviour. Server-side substring search has no existing pattern in this codebase; client renderers index simplexName themselves once it appears in the JSON shape. External-link share (preferring simplex:/name… form over the raw link when the contact has a simplexName) lands in the next commit. * chat: share/copy output prefers simplexName when present When a contact or group has a simplex_name stored, the share-link render path emits the canonical simplex:/name... URI (via strEncode) instead of the underlying connection link. Falls back to the existing link rendering when simplexName is Nothing. Final commit of the ConnectTarget plumbing chain: end-to-end users can now (a) connect via @alice.simplex / #group.simplex with the agent layer carrying the name, (b) see the simplex name on the contact/group records and in viewConnectionPlan, (c) share the contact using the namespace-canonical form rather than the raw URI. * deps: bump simplexmq for boundedNonSpace + drop unused FromField * chat: simplex_name partial indexes are UNIQUE A simplex name is a stable, per-user identity (one name → one contact or group). Without a unique constraint, a later writer that populates the column twice for the same name would silently produce two matching rows, and getContactBySimplexName/getGroupInfoBySimplexName would return whichever the planner picks first. Promote the partial indexes added in M20260603 to UNIQUE before any caller wires the writes. Predicate (WHERE simplex_name IS NOT NULL) already scopes the constraint to rows that opted in. * chat: regen Postgres schema dump for UNIQUE simplex_name indexes Follow-up to f71c579c. The SchemaDump test runs against SQLite only; the parallel PostgresSchemaDump suite gates on -fclient_postgres and a running localhost PG instance, which this environment doesn't have. Updated the Postgres schema dump by hand to mirror the migration change (two lines: CREATE INDEX → CREATE UNIQUE INDEX). * chat: CESimplexNameNotFound for name lookup misses connectPlanName now distinguishes "name not found" from "connection link is invalid". CEInvalidConnReq's message ("Connection link is invalid, possibly it was created in a previous version") was misleading when a user typed @alice.simplex against a database that simply has no contact by that name. The two "missing prepared link" cases stay on CEInvalidConnReq — the lookup found a row but the stored link is unusable, which is closer to the existing semantics. The two truly-missing cases (no contact found / no group found) move to CESimplexNameNotFound, which also surfaces the name back to the client for a precise UX. * chat: exclude soft-deleted contacts from idx_contacts_simplex_name The lookup `getContactBySimplexName` (Store/Direct.hs:781) filters `AND deleted = 0`, but the index predicate `WHERE simplex_name IS NOT NULL` covered tombstoned rows too. Forward-compat trap: once writers land a non-Nothing simplex_name, soft-deleting a contact would block re-claiming its name (UNIQUE conflict) even though the lookup reports the slot as free. Tighten the partial-index predicate to also require deleted = 0 so the constraint scope matches the live-lookup scope. Groups have no soft- delete column, so their index stays as-is. * chat: connectPlanName dispatches on nameType first Previously the function always probed contacts.simplex_name first and fell through to groups for NTPublicGroup misses. But the discriminator (`@`/`#`) is embedded in the stored bytes via strEncode, so an `#group.simplex` lookup can never match a contact row. Reorder to case on nameType up front, saving one DB query and one withFastStore transaction acquire on the group path. * chat: CESimplexNameUnprepared for found-but-no-link cases + member-removed filter connectPlanName previously threw CEInvalidConnReq when a name lookup hit a contact / group row whose preparedContact / preparedGroup was NULL. The error message ("Connection link is invalid, possibly it was created in a previous version") was wrong: the name resolved fine, the device just has no link material to reconnect via (typical for a contact created via the XInfo handler rather than the prepare path). Introduce CESimplexNameUnprepared SimplexNameInfo for this case. Also mirror the link-based path's gPlan (Commands.hs:4133) for groups whose membership state is GSMemRemoved — return CESimplexNameNotFound rather than GLPKnown for a removed-member group, since GLPKnown for removed members would be inconsistent with how /_connect plan over a short link handles the same situation. * chat: fix stale CRITICAL comment in saveConnInfo Comment claimed SEDBException is re-thrown as CRITICAL but only SEDBBusyError is (via the `critical` helper at Subscriber.hs:136 and the showCritical branch at :1695). Updated to describe the actual behaviour. * chat: fix misleading decodeSimplexName docstring The comment described "@alice.simplex" as the column's surface form, but ToField SimplexNameInfo writes the canonical strEncode output ("simplex:/name@alice.simplex"). Aligns the docstring with what the column actually holds. * chat: drop redundant T.unpack in CESimplexName* error rendering plain has a Text instance (verified by sibling simplexNameLine at View.hs:2176 which uses it directly). The T.unpack in the new error renderings was inconsistent with the same-feature helper. Cosmetic cleanup. * deps: bump simplexmq for resolveSimplexName * chat: simplexName field on Profile, GroupProfile, LocalProfile Adds a Maybe SimplexNameInfo field to the wire-level Profile and GroupProfile (and their DB sibling LocalProfile). JSON instances are TH-derived with omitNothingFields = True, so the new optional field is auto-handled and old peers / old JSON without the key decode as Nothing. Existing record-construction sites are set to simplexName = Nothing as a placeholder. Outgoing dissemination (userProfileDirect / userProfileInGroup) and incoming persistence wire-up land in follow-up commits. redactedMemberProfile passes the field through, matching how peerType is preserved. * chat: load LocalProfile.simplexName from simplex_name column Populate the embedded LocalProfile.simplexName field for the user's own profile and for peer Contact / GroupInfo from the existing simplex_name columns on contacts and groups. Previously every DB read set this field to Nothing (Task 1 placeholder), so downstream consumers that work off LocalProfile / GroupProfile (e.g., userProfileDirect / userProfileInGroup that build outgoing XInfo / XGrpInfo via fromLocalProfile) saw Nothing unconditionally. Scope is limited to the rows where the simplex_name column actually exists: contacts (per-user) and groups (per-user). Sites that only read contact_profiles / group_profiles (toContactRequest, toContactProfile, toGroupProfile, rowToLocalProfile) remain Nothing; Task 3 adds the profile-table columns and wires them up. * chat: test outgoing Profile carries simplexName from User profile userProfileDirect, userProfileInGroup' and redactedMemberProfile already pass simplexName through via fromLocalProfile (Task 1) once the embedded LocalProfile field is populated (previous commit). Lock that behavior in with focused unit tests: - userProfileDirect with Just simplexName -> wire Profile.simplexName Just - userProfileDirect with Nothing -> wire Nothing - userProfileDirect with an incognito Profile overlay -> wire Nothing (incognito identity must not leak the user's registered name) - userProfileInGroup' pass-through - redactedMemberProfile pass-through (forwarded member profiles) * chat: clarify groups.simplex_name stopgap comment Drop the asymmetric toContact comment (the mirror is now obvious post-Task-1) and rewrite the toGroupInfo stopgap comment to reflect the actual semantics: groups.simplex_name is per-user locally-known, mirrored into groupProfile as a stopgap until group_profiles.simplex_name lands. * chat: add simplex_name to contact_profiles and group_profiles Adds nullable simplex_name TEXT column and partial UNIQUE (user_id, simplex_name) index to both contact_profiles and group_profiles tables. Distinct from contacts.simplex_name / groups.simplex_name (M20260603), which carry the user's locally-known label set by the prepare-via-name path; the new columns will carry the peer's broadcast claim received via XInfo / XGrpInfo (wired up in following commits). * chat: persist peer-claimed simplexName from incoming profiles Write paths: - updateContactProfile_' / updateGroupProfile_ now set the new contact_profiles.simplex_name / group_profiles.simplex_name columns from Profile.simplexName / GroupProfile.simplexName respectively. - createContact_ INSERT writes Profile.simplexName to the new contact_profiles column (separate from the existing simplexName arg, which still writes contacts.simplex_name — the user's locally-known label). Read paths (closing Task 2's deferred sites): - toContact splits simplex_name reads: Contact.simplexName from contacts.simplex_name (existing); LocalProfile.simplexName from contact_profiles.simplex_name (new column). - toGroupInfo similarly splits: GroupInfo.simplexName from groups.simplex_name; groupProfile.simplexName from group_profiles.simplex_name. - ProfileRow / rowToLocalProfile, toContactRequest, getUserContactProfiles, toGroupProfile, getProfileById, groupMemberQuery, getGroupAndMember_, saveRcvChatItem-related quotes — all extended to read p.simplex_name and decode it into LocalProfile.simplexName / GroupProfile.simplexName. Conflict handling (Decision B): - clearConflictingContactProfileSimplexName_ / *Group* helpers do an atomic UPDATE-with-RETURNING that NULLs simplex_name on any other row in the same user that would collide on the partial UNIQUE index, returning the displaced row's display_name. - updateContactProfileWithConflict / updateGroupProfileWithConflict bundle clear+update in one transaction. - processContactProfileUpdate / xGrpInfo invoke the *WithConflict variants and emit CEvtSimplexNameConflict when a displacement happened (with the claiming and displaced display names). Adds ChatEvent CEvtSimplexNameConflict and SimplexNameConflictEntity (SNCEContact / SNCEGroup) with JSON instances and View.hs rendering. * chat: fix review findings on simplex_name persistence - updateUserProfile no longer writes contact_profiles.simplex_name on the user's own row (the column is reserved for peer claims; the user's broadcastable name lives on contacts.simplex_name via uct.simplex_name). - updateMemberContactProfile_'/Reset_' now write simplex_name; new updateMemberProfileWithConflict / updateContactMemberProfileWithConflict variants run conflict-clear and return the displaced name, with processMemberProfileUpdate emitting CEvtSimplexNameConflict. - createContact_ runs conflict-clear before INSERT to avoid UNIQUE constraint violations on first-write peer collisions, returning the displaced name; createPreparedContact / createDirectContact thread it through to APIPrepareContact and saveConnInfo XInfo for event emission. - groups conflict-clear takes ProfileId directly (avoids the NOT IN (NULL) silent-noop edge case when groups.group_profile_id is ON DELETE SET NULL). - Moves clearConflictingContactProfileSimplexName_ to Shared.hs so createContact_ can call it without inducing a circular import. * chat: resolveOnUserServers iterates user SMP servers for RSLV * chat: connectPlanName falls back to RSLV when local lookup misses * chat: RSLV-resolved NameRecord dispatched through prepared row dispatchResolvedRecord now picks the first nrContactLinks (NTContact) or nrChannelLinks (NTPublicGroup) entry from the resolved record, decodes it as AConnShortLink, fetches the short-link data, and eagerly calls createPreparedContact / createPreparedGroup with the simplex_name set. Returning CPContactAddress (CAPKnown ct) / CPGroupLink (GLPKnown g ...) mirrors the local-store-hit branch of connectPlanName: hit and miss converge on the same plan shape, so the connectWithPlan caller cannot distinguish where the prepared row came from. Threading uses the existing Maybe SimplexNameInfo parameter added in c6f26150 for the local-prepare path -- no new write path or transient carrier. Pure helper firstNameLink is extracted and exported so the link-picker contract is testable without a DB / agent. ResolveNameTests gains five cases covering the per-type selection, the first-link policy, and the empty-list to CESimplexNameNotFound collapse. * chat: regen query plans after simplex_name plumbing * chat: register ConnectTarget + CEvtSimplexNameConflict in bot docs Bot API docs generator failed with "Undefined type: ConnectTarget" since f2394d121 (prior plan) flipped APIConnectPlan/Connect from Maybe AConnectionLink to Maybe ConnectTarget without updating bots/src/API/Docs/*. Also adds SimplexNameConflictEntity (new in cd0de9659) and documents the CEvtSimplexNameConflict event for peer-name displacement notifications. Regenerates the affected markdown / TypeScript / Python artefacts. * deps: bump simplexmq for NameRecord reshape; update consumers simplexmq 5ee014dd reshaped NameRecord to align with the Python resolver JSON: nrChannelLinks/nrContactLinks (lists of NameLink) became nrSimplexChannel/nrSimplexContact (Maybe Text); nrDisplayName became nrName; nrResolver was added; the NameLink wrapper type and nrIsTest/ nrExpiry/nrAdminAddress/nrAdminEmail fields were dropped. Update dispatchResolvedRecord destructure and firstNameLink signature to the new Maybe Text shape, and refresh the ResolveNameTests fixtures and assertions accordingly. * chat: resolveOnUserServers iterates only on transport errors Privacy: every miss previously broadcast the candidate name to every enabled SMP server. Now only NETWORK / TIMEOUT failures fall through to the next server; definite resolver answers (NAME / AUTH / CMD PROHIBITED / other ERR) stop iteration. * chat: document why groups simplex_name index has no soft-delete filter The contacts simplex_name index filters on (deleted = 0); the groups index has no analogous filter because the groups table has no `deleted` column. Groups are hard-deleted by deleteGroup, so the asymmetry is intentional. The remaining "removed member, row retained" edge case is flagged in the lookup comment for follow-up. * chat: document connections.simplex_name as transient carrier Audit flagged the column as "INSERTed but never UPDATEd". This is by design per the prior plan's connect-via-plan flow: the column is a transient carrier between connection-creation and contact-creation. After the Contact row is created via XInfo handling, contacts.simplex_name is the source of truth and the connections value is a historical snapshot. Documents the intent so future readers don't reflag it. * chat: extract surfaceSimplexNameConflict helper Six call sites duplicated the same forM_ ((,) <$> claim <*> displaced) shape emitting CEvtSimplexNameConflict. Extract to a single helper so future call sites don't drift on whether to emit, and so the conflict event shape change (post-Task-3 SimplexNameConflictEntity split into SNCEContact / SNCEGroup) propagates through one site. * chat: APIVerifySimplexName command + CEvtSimplexNameUnverified warning Addresses the TOFU vulnerability where peer-claimed simplex_name was accepted unverified. Adds: - contacts.simplex_name_verified_at + groups.simplex_name_verified_at (M20260606_simplex_name_verified) - APIVerifySimplexName ChatRef command: RSLV-resolves the claimed name and compares the resolved link to the peer's stored connection link; on match writes verified_at and emits CEvtSimplexNameVerified; on mismatch emits CEvtSimplexNameVerifyFailed - CEvtSimplexNameUnverified passive warning emitted on incoming XInfo / XGrpInfo when a name claim arrives without a current verification - updateContactProfileWithConflict / updateGroupProfileWithConflict clear simplex_name_verified_at whenever the peer's claim transitions (any value change including Nothing<->Just): the prior verification was bound to the prior claim. UI can surface the unverified indicator next to a contact / group's name, and prompt the user to invoke the verify command. This shifts the security model from "TOFU + last-writer-wins" to "TOFU + on-demand RSLV verification". * chat: register APIVerifySimplexName + verify events in bot docs ebe90f716 added the verify command + events + SimplexNameVerifyFailReason type without touching bots/src/API/Docs/. Mirrors commit 0d7ea8061 which addressed the same gap for ConnectTarget. Regenerates the affected markdown / TypeScript / Python artefacts. * chat: bump simplexmq pin + document cross-table simplex_name discriminator Pin bump 5ee014dd -> c9c2d19 picks up the 8 simplexmq commits since the last bump (parseBare lowercase fix, forwarded-param cleanup, ServerTests + agent end-to-end tests, TldRegistries removal, SNRC ABI decoder, NameRecord/NameOwner module extraction). Adds a brief comment on clearConflictingContactProfileSimplexName_ explaining why the audit's flagged cross-table collision (between contact_profiles.simplex_name and group_profiles.simplex_name) is structurally impossible: SimplexNameInfo's strEncode prefixes contact names with '@' and group names with '#', so the stored bytes never overlap between the two tables. Query-plan regen deferred (the test is non-deterministic in CI / dev sandbox — see prior 6c990696c). * deps: bump simplexmq for HTTP resolver; adapt NameRecord consumers simplexmq 92b3d049 reshaped NameRecord text fields from Maybe Text to Text (empty string sentinel). Adapt firstNameLink to take Text directly and treat T.null as "absent". dispatchResolvedRecord destructure unchanged; passes the text values straight through. apiVerifySimplexName switches from Just/Nothing pattern to a T.null guard with the same UX. Test fixtures updated. * deps: bump simplexmq for multi-link NameRecord; adapt consumers * core: treat RSLV CMD UNKNOWN as no name-resolution support * core: fix contact-by-connection query missing simplex_name_verified_at * deps: update sha256map for simplexmq f555e9af pin * core: iterate past RSLV-unsupported name servers * core: filter RSLV servers by operator enablement * core: align resolver docs/tests with RSLV errors * deps: bump simplexmq to df1aa24c * refactor(names): agent resolution + one error type Adopt the simplexmq names rework (PR #7045): name resolution is now owned by the agent (resolveSimplexName picks a names-role server), so the chat-side iteration is removed - delete ResolveError, iterateResolvers, resolveOnUserServers, enabledSMPServersForUser and resolveErrorToChatError. One error type: resolver/agent failures flow through ChatErrorAgent; remove the CEvtSimplexName* events, SimplexNameVerifyFailReason, SimplexNameConflictEntity and CESimplexNameResolverUnavailable. APIVerifySimplexName returns CRSimplexNameVerified (verified::Bool), mirroring CRConnectionVerified. connectPlan handles the name target directly; updateProfile WithConflict aliases collapsed into the plain functions. Add the per-operator "names" SMP server role (migration 20260612_smp_role_names, official operator on by default) feeding ServerRoles.names -> UserServers.nameSrvs. Bump simplexmq pin to ce69adfd and regenerate sha256map.nix. * fix(store): match chat_schema.sql to sqlite 3.46+ indent The schema-dump test renders the partial-index WHERE via the sqlite3 CLI; sqlite >=3.46 wraps a multi-condition WHERE onto two lines ("IS NOT NULL" + indented "AND ...") where 3.45 kept it on one. The committed schema was generated with 3.45, so CI (newer sqlite) failed the comparison on idx_contacts_simplex_name. Regenerated with the newer formatter; only that one WHERE clause changes. * feat(operators): warn when no server resolves names Mirror USWNoChatRelays: validateUserServers emits USWNoNamesServers when no enabled server of an enabled operator carries the SMP names role. noNamesServersWarns is self-contained with local predicates, matching the sibling noChatRelaysWarns; noServersErrs is untouched. * test(operators): expect USWNoNamesServers warning in no-servers cases * fix(store): single-line simplex_name WHERE to match CI sqlite (<=3.45) * chore: bump simplexmq pin to 6843b14c * refactor(store): consolidate names migrations into one Unshipped feature - merge the four incremental simplex_name migrations (0603/0604/0606/0612) into a single M20260603_simplex_name. The combined UP applies the ALTERs/indexes in the same order, so the resulting schema is byte-identical (verified by SchemaDump on SQLite and pg_dump on Postgres). * update simplexmq * plan for name resolution * update types and schema * simpler resolution, name proofs * simplexmq * generate bot types, schema, unStrJSON, fix tests * ad hoc link comparison, create short link * update simplexmq * remove same link, use simplexmq instead * split verify API for contacts and public groups * remove comment * simplify warnings * test: remove unused * remove cute language * type name * remove spurious comments * refactor setting user name * refactor setting user name * remove trivial tests * refactor * remove tests using pre-short-link addresses * rename * move names * bots api * refactor more * refactor * refactor to another type * load own names * read short links when looking up by name * renames * refactor verification * mapM_ * update api types * change field for name * rename columns * api types, schema * renames * fix links * simplify * remove comments * rename fields * simplify * remove proof from channel addres * refactor * name resolution test * change tests * fix tests * fix tests * fix plan for names * test * test verification status * bot api * fix tests * update bot api types * query plan * add api for setting public group access * android, desktop, ios: connect via SimpleX name (#7068) * android, desktop, ios: connect via SimpleX name * android, desktop, ios: open known contact on name lookup; surface prepared contact Name search opens the contact (not list-filter); resolved/prepared contacts and groups are added to the chat list so they're visible and openable. Kotlin compile-verified; iOS edits pattern-matched, pending Xcode build. * feat(names): UI names role + agent NAME error Parity with the core names rework (#7045): - Add `names` to ServerRoles (Android + iOS) and a per-operator "To resolve names" toggle under the SMP section (xftp has no names role; the shared ServerRoles field stays false there). - Mirror the new agent error: NameErrorType + a NAME case on both AgentErrorType and ProtocolErrorType (the SMP ErrorType mirror), so the new SMP/agent NAME errors decode instead of crashing the decoder. - Remove ChatErrorType.SimplexNameResolverUnavailable (deleted in core) and repoint its "name resolution unavailable" alert to the agent NAME NO_SERVERS error, reusing the existing strings. Android (multiplatform) compiles clean; iOS mirrors the same changes (builds in Xcode). * feat(names): UI warning when no server resolves names Mirror core USWNoNamesServers: add the NoNamesServers variant to UserServersWarning (Kotlin sealed class + Swift enum) and its globalWarning / globalServersWarning branch, rendered by the existing ServersWarningFooter / ServersWarningView. Matches the noChatRelays warning exactly. * fix(servers): show all validation errors and warnings, not just the first globalServersError/Warning returned only the first entry, so a second warning (e.g. no names servers behind no chat relays) or a second error (e.g. no XFTP servers behind no SMP servers) was never displayed. Make them return all entries (globalServersErrors/Warnings) and render one footer row each, across the three combined-footer views. Per-protocol SMP/XFTP footers are unchanged. * docs(names): add SimpleX name UI plan * feat(names): add name model fields + SimplexName helpers * feat(names): verify + set-name API & responses * docs(names): bump core sync to 5008b4e62 * feat(names): show name + verification on chat info * feat(names): add Verify SimpleX names privacy toggle * feat(names): add set-name screens (user + channel) * update ui * fix kotlin * fix codable * fix ios * fix errors * api in UI * send name as string in protocol * update simplexmq, capitalize * verify that name is in profile for own and known contacts and channels as condition of name resolution * update simplexmq --------- Co-authored-by: Evgeny Poberezkin Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> * add log * bot types * finalize renames, ui alerts * more renames * kotlin alerts * show set name * alerts * texts, icons, footers * move JSON parsing to a persistent thread with large stack * simplexmq * use uikit alerts * remove comment * move name verification to more privacy * change icon on verify names setting * verify name proof * nix shas * show verified names * better alert when saving names * revert breaking field name change * simplexmq * clean up * remove unused * remove empty line * fix error * simplify * use domain without prefix in profiles and in database, rename fields and types * rename types * fix JSON name * pass verified domain to prepare API * fix prepare api * fix ios encoding * enable name resolution via Flux servers * fix test --------- Co-authored-by: Evgeny Poberezkin Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- apps/ios/Shared/Model/AppAPITypes.swift | 40 +- apps/ios/Shared/Model/SimpleXAPI.swift | 88 ++- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 78 +++ .../Views/Chat/ChatItem/MsgContentView.swift | 4 +- .../Chat/Group/ChannelWebAccessView.swift | 2 +- .../Views/Chat/Group/GroupChatInfoView.swift | 50 ++ .../Shared/Views/ChatList/ChatListView.swift | 13 +- .../Views/NewChat/NewChatMenuButton.swift | 13 +- .../Shared/Views/NewChat/NewChatView.swift | 53 +- .../NetworkAndServers/NetworkAndServers.swift | 33 +- .../NetworkAndServers/OperatorView.swift | 18 +- .../ProtocolServersView.swift | 14 +- .../Views/UserSettings/PrivacySettings.swift | 4 + .../Views/UserSettings/SettingsView.swift | 2 + .../Views/UserSettings/UserAddressView.swift | 76 +++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +- apps/ios/SimpleXChat/APITypes.swift | 86 ++- apps/ios/SimpleXChat/ChatTypes.swift | 82 ++- .../chat/simplex/common/model/ChatModel.kt | 48 +- .../chat/simplex/common/model/SimpleXAPI.kt | 193 ++++++- .../simplex/common/views/chat/ChatInfoView.kt | 14 + .../common/views/chat/SimplexNameView.kt | 94 ++++ .../views/chat/group/ChannelWebPageView.kt | 2 +- .../views/chat/group/GroupChatInfoView.kt | 44 ++ .../common/views/chat/item/TextItemView.kt | 11 +- .../common/views/chatlist/ChatListView.kt | 15 +- .../common/views/newchat/ConnectPlan.kt | 32 +- .../common/views/newchat/NewChatSheet.kt | 15 +- .../common/views/newchat/NewChatView.kt | 22 +- .../views/usersettings/PrivacySettings.kt | 5 + .../views/usersettings/SetSimplexNameView.kt | 79 +++ .../views/usersettings/UserAddressView.kt | 33 ++ .../networkAndServers/NetworkAndServers.kt | 50 +- .../networkAndServers/OperatorView.kt | 46 +- .../networkAndServers/ProtocolServersView.kt | 10 +- .../commonMain/resources/MR/base/strings.xml | 23 + .../src/Directory/Service.hs | 8 +- bots/api/COMMANDS.md | 20 +- bots/api/TYPES.md | 97 +++- bots/src/API/Docs/Commands.hs | 10 +- bots/src/API/Docs/Responses.hs | 2 + bots/src/API/Docs/Types.hs | 17 +- bots/src/API/TypeInfo.hs | 3 + cabal.project | 2 +- .../types/typescript/src/commands.ts | 12 +- .../types/typescript/src/types.ts | 95 +++- .../src/simplex_chat/types/_commands.py | 12 +- .../src/simplex_chat/types/_types.py | 70 ++- plans/2026-06-25-name-resolution.md | 506 ++++++++++++++++++ plans/2026-06-27-namespace-ui-display-set.md | 243 +++++++++ scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 7 + src/Simplex/Chat/Badges.hs | 23 +- src/Simplex/Chat/Controller.hs | 27 +- src/Simplex/Chat/Core.hs | 2 +- src/Simplex/Chat/Library/Commands.hs | 319 ++++++++--- src/Simplex/Chat/Library/Internal.hs | 49 +- src/Simplex/Chat/Library/Subscriber.hs | 37 +- src/Simplex/Chat/Names.hs | 61 +++ src/Simplex/Chat/Operators.hs | 18 +- src/Simplex/Chat/Operators/Presets.hs | 4 +- src/Simplex/Chat/ProfileGenerator.hs | 2 +- src/Simplex/Chat/Store/Connections.hs | 15 +- src/Simplex/Chat/Store/ContactRequest.hs | 4 +- src/Simplex/Chat/Store/Direct.hs | 86 ++- src/Simplex/Chat/Store/Groups.hs | 125 +++-- src/Simplex/Chat/Store/Messages.hs | 10 +- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20260603_simplex_name.hs | 38 ++ .../Store/Postgres/Migrations/chat_schema.sql | 19 +- src/Simplex/Chat/Store/Profiles.hs | 74 ++- src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20260603_simplex_name.hs | 37 ++ .../SQLite/Migrations/chat_query_plans.txt | 211 +++++--- .../Store/SQLite/Migrations/chat_schema.sql | 14 +- src/Simplex/Chat/Store/Shared.hs | 79 +-- src/Simplex/Chat/Types.hs | 96 +++- src/Simplex/Chat/View.hs | 96 +++- tests/Bots/BroadcastTests.hs | 2 +- tests/Bots/DirectoryTests.hs | 2 +- tests/ChatClient.hs | 21 +- tests/ChatTests/Direct.hs | 6 +- tests/ChatTests/Names.hs | 143 +++++ tests/ChatTests/Profiles.hs | 91 +--- tests/ChatTests/Utils.hs | 2 +- tests/MarkdownTests.hs | 4 +- tests/NameResolver.hs | 83 +++ tests/OperatorTests.hs | 6 +- tests/PostgresSchemaDump.hs | 4 +- tests/ProtocolTests.hs | 5 +- tests/SchemaDump.hs | 4 +- tests/Test.hs | 6 + 92 files changed, 3532 insertions(+), 815 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt create mode 100644 plans/2026-06-25-name-resolution.md create mode 100644 plans/2026-06-27-namespace-ui-display-set.md create mode 100644 src/Simplex/Chat/Names.hs create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20260603_simplex_name.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20260603_simplex_name.hs create mode 100644 tests/ChatTests/Names.hs create mode 100644 tests/NameResolver.hs diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index a5a56174b1..9e27c6fa16 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -84,6 +84,7 @@ enum ChatCommand: ChatCmdProtocol { case apiLeaveGroup(groupId: Int64) case apiListMembers(groupId: Int64) case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) + case apiSetPublicGroupAccess(groupId: Int64, access: PublicGroupAccess) case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole) case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) case apiDeleteGroupLink(groupId: Int64) @@ -131,8 +132,8 @@ enum ChatCommand: ChatCmdProtocol { case apiSetConnectionIncognito(connId: Int64, incognito: Bool) case apiChangeConnectionUser(connId: Int64, userId: Int64) case apiConnectPlan(userId: Int64, connLink: String, linkOwnerSig: LinkOwnerSig?) - case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) - case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData) + case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain?) + case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain?) case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) case apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) case apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) @@ -155,6 +156,9 @@ enum ChatCommand: ChatCmdProtocol { case apiAddMyAddressShortLink(userId: Int64) case apiSetProfileAddress(userId: Int64, on: Bool) case apiSetAddressSettings(userId: Int64, addressSettings: AddressSettings) + case apiSetUserDomain(userId: Int64, simplexDomain: String?) + case apiVerifyContactDomain(contactId: Int64) + case apiVerifyGroupDomain(groupId: Int64) case apiAcceptContact(incognito: Bool, contactReqId: Int64) case apiRejectContact(contactReqId: Int64) // WebRTC calls @@ -346,8 +350,8 @@ enum ChatCommand: ChatCmdProtocol { case let .apiConnectPlan(userId, connLink, linkOwnerSig): let sigStr = if let linkOwnerSig { " sig=\(encodeJSON(linkOwnerSig))" } else { "" } return "/_connect plan \(userId) \(connLink)\(sigStr)" - case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))" - case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") direct=\(onOff(directLink)) \(encodeJSON(groupShortLinkData))" + case let .apiPrepareContact(userId, connLink, contactShortLinkData, verifiedDomain): return "/_prepare contact \(userId) \(connLink.cmdString)\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(contactShortLinkData))" + case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData, verifiedDomain): return "/_prepare group \(userId) \(connLink.cmdString) direct=\(onOff(directLink))\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(groupShortLinkData))" case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)" case let .apiChangePreparedGroupUser(groupId, newUserId): return "/_set group user #\(groupId) \(newUserId)" case let .apiConnectPreparedContact(contactId, incognito, mc): return "/_connect contact @\(contactId) incognito=\(onOff(incognito))\(maybeContent(mc))" @@ -370,6 +374,10 @@ enum ChatCommand: ChatCmdProtocol { case let .apiAddMyAddressShortLink(userId): return "/_short_link_address \(userId)" case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" case let .apiSetAddressSettings(userId, addressSettings): return "/_address_settings \(userId) \(encodeJSON(addressSettings))" + case let .apiSetUserDomain(userId, simplexDomain): return "/_set domain \(userId)" + (simplexDomain.map { " " + $0 } ?? "") + case let .apiSetPublicGroupAccess(groupId, access): return "/_public group access #\(groupId) \(encodeJSON(access))" + case let .apiVerifyContactDomain(contactId): return "/_verify domain @\(contactId)" + case let .apiVerifyGroupDomain(groupId): return "/_verify domain #\(groupId)" case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)" case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))" @@ -481,6 +489,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiLeaveGroup: return "apiLeaveGroup" case .apiListMembers: return "apiListMembers" case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" + case .apiSetPublicGroupAccess: return "apiSetPublicGroupAccess" case .apiCreateGroupLink: return "apiCreateGroupLink" case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" case .apiDeleteGroupLink: return "apiDeleteGroupLink" @@ -551,6 +560,9 @@ enum ChatCommand: ChatCmdProtocol { case .apiAddMyAddressShortLink: return "apiAddMyAddressShortLink" case .apiSetProfileAddress: return "apiSetProfileAddress" case .apiSetAddressSettings: return "apiSetAddressSettings" + case .apiSetUserDomain: return "apiSetUserDomain" + case .apiVerifyContactDomain: return "apiVerifyContactDomain" + case .apiVerifyGroupDomain: return "apiVerifyGroupDomain" case .apiAcceptContact: return "apiAcceptContact" case .apiRejectContact: return "apiRejectContact" case .apiSendCallInvitation: return "apiSendCallInvitation" @@ -960,6 +972,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole) case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool) case groupUpdated(user: UserRef, toGroup: GroupInfo) + case contactDomainVerified(user: UserRef, contact: Contact, verificationFailure: String?) + case groupDomainVerified(user: UserRef, groupInfo: GroupInfo, verificationFailure: String?) case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink) case groupLink(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink) case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo) @@ -1015,6 +1029,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case .membersRoleUser: "membersRoleUser" case .membersBlockedForAllUser: "membersBlockedForAllUser" case .groupUpdated: "groupUpdated" + case .contactDomainVerified: "contactDomainVerified" + case .groupDomainVerified: "groupDomainVerified" case .groupLinkCreated: "groupLinkCreated" case .groupLink: "groupLink" case .groupLinkDeleted: "groupLinkDeleted" @@ -1066,6 +1082,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)") case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)") case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) + case let .contactDomainVerified(u, contact, verificationFailure): return withUser(u, "contact: \(contact)\nverificationFailure: \(verificationFailure ?? "ok")") + case let .groupDomainVerified(u, groupInfo, verificationFailure): return withUser(u, "groupInfo: \(groupInfo)\nverificationFailure: \(verificationFailure ?? "ok")") case let .groupLinkCreated(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)") case let .groupLink(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)") case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo)) @@ -1383,7 +1401,7 @@ enum InvitationLinkPlan: Decodable, Hashable { } enum ContactAddressPlan: Decodable, Hashable { - case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?) + case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?, verifiedDomain: SimplexDomain?) case ownLink case connectingConfirmReconnect case connectingProhibit(contact: Contact) @@ -1398,7 +1416,7 @@ public struct GroupShortLinkInfo: Decodable, Hashable { } enum GroupLinkPlan: Decodable, Hashable { - case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?) + case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?, verifiedDomain: SimplexDomain?) case ownLink(groupInfo: GroupInfo) case connectingConfirmReconnect case connectingProhibit(groupInfo_: GroupInfo?) @@ -1766,14 +1784,15 @@ struct ServerOperator: Identifiable, Equatable, Codable { serverDomains: ["simplex.im"], conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false), enabled: true, - smpRoles: ServerRoles(storage: true, proxy: true), - xftpRoles: ServerRoles(storage: true, proxy: true) + smpRoles: ServerRoles(storage: true, proxy: true, names: true), + xftpRoles: ServerRoles(storage: true, proxy: true, names: false) ) } struct ServerRoles: Equatable, Codable { var storage: Bool var proxy: Bool + var names: Bool } struct UserOperatorServers: Identifiable, Equatable, Codable { @@ -1800,8 +1819,8 @@ struct UserOperatorServers: Identifiable, Equatable, Codable { serverDomains: [], conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false), enabled: false, - smpRoles: ServerRoles(storage: true, proxy: true), - xftpRoles: ServerRoles(storage: true, proxy: true) + smpRoles: ServerRoles(storage: true, proxy: true, names: true), + xftpRoles: ServerRoles(storage: true, proxy: true, names: false) ) } set { `operator` = newValue } @@ -1824,6 +1843,7 @@ struct UserOperatorServers: Identifiable, Equatable, Codable { public enum UserServersWarning: Decodable { case noChatRelays(user: UserRef?) + case noNamesServers(user: UserRef?) } enum UserServersError: Decodable { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ea2de31569..3b610663dc 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1083,6 +1083,24 @@ private func apiConnectResponseAlert(_ r: APIResult) -> Alert { title: "Unsupported connection link", message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." ) + case let .error(.simplexDomainNotReady(domain, err)): + switch err { + case .noValidLink: + mkAlert( + title: "No valid link", + message: "The SimpleX name \(domain.fullDomainName) is registered, but it has no valid link." + ) + case .unknownDomain: + mkAlert( + title: "Unconfirmed name", + message: "The SimpleX name \(domain.fullDomainName) is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." + ) + } + case .errorAgent(.NO_NAME_SERVERS): + mkAlert( + title: "SimpleX name error", + message: "None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." + ) case .errorAgent(.SMP(_, .AUTH)): mkAlert( title: "Connection error (AUTH)", @@ -1113,6 +1131,24 @@ private func apiConnectResponseAlert(_ r: APIResult) -> Alert { } else { connectionErrorAlert(r) } + case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))): + switch nameErr { + case .NOT_FOUND: + mkAlert( + title: "Name not found", + message: "This SimpleX name is not registered. Please check the name." + ) + case .NO_RESOLVER: + mkAlert( + title: "SimpleX name error", + message: "Server \(serverAddress) does not support name resolution. Configure servers, or use a connection link." + ) + case let .RESOLVER(resolverErr): + mkAlert( + title: "SimpleX name error", + message: "Resolver error: \(resolverErr)" + ) + } default: connectionErrorAlert(r) } } @@ -1156,16 +1192,16 @@ private func connectionErrorAlert(_ r: APIResult) -> Alert { } } -func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) async throws -> ChatData { +func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData { let userId = try currentUserId("apiPrepareContact") - let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData)) + let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain)) if case let .newPreparedChat(_, chat) = r { return chat } throw r.unexpected } -func apiPrepareGroup(connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData) async throws -> ChatData { +func apiPrepareGroup(connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData { let userId = try currentUserId("apiPrepareGroup") - let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, directLink: directLink, groupShortLinkData: groupShortLinkData)) + let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, directLink: directLink, groupShortLinkData: groupShortLinkData, verifiedDomain: verifiedDomain)) if case let .newPreparedChat(_, chat) = r { return chat } throw r.unexpected } @@ -1329,6 +1365,43 @@ func apiSetProfileAddress(on: Bool) async throws -> User? { } } +// name is the encoded SimplexName (e.g. "@alice.simplex"); nil clears it +// owner-specific SNENoValidLink wording; everything else reuses the general apiConnectResponseAlert +func showSetSimplexNameError(_ r: APIResult, isChannel: Bool) { + if case let .error(.simplexDomainNotReady(domain, .noValidLink)) = r.unexpected { + let format = isChannel + ? NSLocalizedString("The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page.", comment: "alert message") + : NSLocalizedString("The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.", comment: "alert message") + showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName)) + } else { + AlertManager.shared.showAlert(apiConnectResponseAlert(r)) + } +} + +func apiSetUserDomain(_ simplexDomain: String?) async throws -> User { + let userId = try currentUserId("apiSetUserDomain") + let r: APIResult = await chatApiSendCmd(.apiSetUserDomain(userId: userId, simplexDomain: simplexDomain)) + switch r { + case let .result(.userProfileUpdated(user, _, _, _)): return user + case let .result(.userProfileNoChange(user)): return user + default: + showSetSimplexNameError(r, isChannel: false) + throw r.unexpected + } +} + +func apiVerifyContactDomain(_ contactId: Int64) async throws -> (Contact, String?) { + let r: ChatResponse2 = try await chatSendCmd(.apiVerifyContactDomain(contactId: contactId)) + if case let .contactDomainVerified(_, contact, verificationFailure) = r { return (contact, verificationFailure) } + throw r.unexpected +} + +func apiVerifyGroupDomain(_ groupId: Int64) async throws -> (GroupInfo, String?) { + let r: ChatResponse2 = try await chatSendCmd(.apiVerifyGroupDomain(groupId: groupId)) + if case let .groupDomainVerified(_, groupInfo, verificationFailure) = r { return (groupInfo, verificationFailure) } + throw r.unexpected +} + func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? { let r: ChatResponse1 = try await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences)) if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact } @@ -1995,6 +2068,13 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws throw r.unexpected } +func apiSetPublicGroupAccess(_ groupId: Int64, access: PublicGroupAccess) async throws -> GroupInfo { + let r: APIResult = await chatApiSendCmd(.apiSetPublicGroupAccess(groupId: groupId, access: access)) + if case let .result(.groupUpdated(_, toGroup)) = r { return toGroup } + showSetSimplexNameError(r, isChannel: true) + throw r.unexpected +} + func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) if case let .result(.groupLinkCreated(_, _, groupLink)) = r { return groupLink } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index fdd1dc8a6a..9988b30d4e 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -392,6 +392,26 @@ struct ChatInfoView: View { .lineLimit(3) .padding(.bottom, 2) } + if let domain = contact.profile.contactDomain, + contact.profile.contactDomainVerified != nil || domain.proof != nil { + SimplexNameView( + simplexName: "@\(domain.shortName)", + verified: contact.profile.contactDomainVerified, + verify: { + do { + let (ct, reason) = try await apiVerifyContactDomain(contact.contactId) + await MainActor.run { + chatModel.updateContact(ct) + contact = ct + } + return (ct.profile.contactDomainVerified, reason) + } catch { + logger.error("apiVerifyContactDomain: \(responseError(error))") + return nil + } + } + ) + } if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) @@ -1361,6 +1381,64 @@ private func deleteNotReadyContact( )) } +struct SimplexNameView: View { + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) var autoVerify = false + let simplexName: String + let verified: Bool? + let verify: () async -> (Bool?, String?)? + @State private var inFlight = false + @State private var showSpinner = false + + var body: some View { + HStack(spacing: 6) { + Text(simplexName) + .font(verified == true ? .subheadline : .system(.subheadline, design: .monospaced)) + .foregroundColor(verified == true ? theme.colors.primary : theme.colors.secondary) + indicator() + } + .padding(.bottom, 2) + .onAppear { if autoVerify && verified == nil { runVerify(manual: false) } } + } + + @ViewBuilder private func indicator() -> some View { + if showSpinner { + ProgressView() + } else if verified == true { + Image(systemName: "checkmark") + } else if verified == false { + Image(systemName: "xmark") + .foregroundColor(.red) + .onTapGesture { runVerify(manual: true) } + } else { + Button { runVerify(manual: true) } label: { + Text("Verify name").font(.subheadline).foregroundColor(theme.colors.primary) + } + } + } + + private func runVerify(manual: Bool) { + if inFlight { return } + inFlight = true + // delay the spinner so a fast result on appear doesn't flash it + Task { + try? await Task.sleep(nanoseconds: 300_000000) + await MainActor.run { if inFlight { showSpinner = true } } + } + Task { + let res = await verify() + await MainActor.run { + inFlight = false + showSpinner = false + // show the reason on a manual run, or on an inconclusive auto run (state stayed nil) + if let (newV, reason) = res, let reason, manual || newV == nil { + showAlert(NSLocalizedString("SimpleX name not verified", comment: "alert title"), message: reason) + } + } + } + } +} + struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { ChatInfoView( diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 11c3c4c3f4..5cb93ad279 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -214,8 +214,8 @@ private func handleTextTaps( var simplex: Bool = false s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in if index >= range.location && index < range.location + range.length { - if let nameInfo = attrs[nameAttrKey] as? SimplexNameInfo { - showUnsupportedNameAlert(nameInfo) + if attrs[nameAttrKey] is SimplexNameInfo { + planAndConnect(s.attributedSubstring(from: range).string, theme: AppTheme.shared, dismiss: false) } else if let url = attrs[linkAttrKey] as? String { linkURL = url browser = attrs[webLinkAttrKey] != nil diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift index dd46b7a117..df0867c470 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift @@ -148,7 +148,7 @@ struct ChannelWebAccessView: View { let existingAccess = pg.publicGroupAccess pg.publicGroupAccess = PublicGroupAccess( groupWebPage: trimmedPage.isEmpty ? nil : trimmedPage, - groupDomain: existingAccess?.groupDomain, + groupDomainClaim: existingAccess?.groupDomainClaim, domainWebPage: existingAccess?.domainWebPage ?? false, allowEmbedding: allowEmbedding ) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 41e24a6ced..358bf6c7d3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -112,6 +112,7 @@ struct GroupChatInfoView: View { // TODO [relays] allow other owners to manage channel link (requires protocol changes to share link ownership) if groupInfo.isOwner && groupLink != nil { channelLinkButton() + channelSimplexNameButton() } else if let link = groupInfo.groupProfile.publicGroup?.groupLink { SimpleXLinkQRCode(uri: link) Button { @@ -331,6 +332,27 @@ struct GroupChatInfoView: View { .lineLimit(4) .fixedSize(horizontal: false, vertical: true) } + if let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess, + let domain = access.groupDomainClaim?.shortName, + groupInfo.groupDomainVerified != nil || access.groupDomainClaim?.proof != nil { + SimplexNameView( + simplexName: "#\(domain)", + verified: groupInfo.groupDomainVerified, + verify: { + do { + let (gInfo, reason) = try await apiVerifyGroupDomain(groupInfo.groupId) + await MainActor.run { + chatModel.updateGroup(gInfo) + groupInfo = gInfo + } + return (gInfo.groupDomainVerified, reason) + } catch { + logger.error("apiVerifyGroupDomain: \(responseError(error))") + return nil + } + } + ) + } if let webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage, let url = URL(string: webPage) { Link(destination: url) { @@ -674,6 +696,34 @@ struct GroupChatInfoView: View { } } + private func channelSimplexNameButton() -> some View { + NavigationLink { + let domain = if let d = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName { "#\(d)" } else { "" } + SetSimplexDomainView( + title: "SimpleX name", + footer: "Let people join via name registered with this channel link.", + prompt: "#channelname.testing", + simplexName: domain, + save: { domain in + do { + var access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?? PublicGroupAccess() + access.groupDomainClaim = domain.map { SimplexDomainClaim(domain: $0) } + let gInfo = try await apiSetPublicGroupAccess(groupInfo.groupId, access: access) + await MainActor.run { + chatModel.updateGroup(gInfo) + groupInfo = gInfo + } + return true + } catch { + return false + } + } + ) + } label: { + Label("SimpleX name", systemImage: "number") + } + } + private func groupLinkDestinationView() -> some View { GroupLinkView( groupId: groupInfo.groupId, diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index d90149c7dd..01887c12be 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -683,8 +683,17 @@ struct ChatListSearchBar: View { searchShowingSimplexLink = true searchChatFilteredBySimplexLink = nil connect(text) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) + case let .name(text, _): + searchFocussed = false + planAndConnect( + text, + theme: theme, + dismiss: false, + cleanup: { + searchText = "" + searchFocussed = false + } + ) case .none: if t != "" { searchFocussed = true diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index f99b03086e..a3e6d2ee2f 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -389,8 +389,17 @@ struct ContactsListSearchBar: View { searchShowingSimplexLink = true searchChatFilteredBySimplexLink = nil connect(text) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) + case let .name(text, _): + searchFocussed = false + planAndConnect( + text, + theme: theme, + dismiss: true, + cleanup: { + searchText = "" + searchFocussed = false + } + ) case .none: if t != "" { searchFocussed = true diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 67fd353ebc..c2154b2f27 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -669,8 +669,9 @@ private struct ConnectView: View { case let .link(text, _, _): pastedLink = text connect(pastedLink) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) + case let .name(text, _): + pastedLink = text + connect(pastedLink) case .none: alert = .newChatSomeAlert(alert: SomeAlert( alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."), @@ -869,7 +870,7 @@ func strIsSimplexLink(_ str: String) -> Bool { enum ConnectTarget { case link(text: String, linkType: SimplexLinkType, linkText: String) - case name(SimplexNameInfo) + case name(text: String, nameInfo: SimplexNameInfo) } func strConnectTarget(_ str: String) -> ConnectTarget? { @@ -878,28 +879,14 @@ func strConnectTarget(_ str: String) -> ConnectTarget? { return if links.count == 1, case let .simplexLink(_, linkType, _, smpHosts) = links[0].format { .link(text: links[0].text, linkType: linkType, linkText: simplexLinkText(linkType, smpHosts)) } else if links.isEmpty, - case let .simplexName(nameInfo) = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } })?.format { - .name(nameInfo) + let nameFt = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } }), + case let .simplexName(nameInfo) = nameFt.format { + .name(text: nameFt.text, nameInfo: nameInfo) } else { nil } } -func showUnsupportedNameAlert(_ nameInfo: SimplexNameInfo) { - let upgrade = " " + NSLocalizedString("Please upgrade the app.", comment: "alert message") - if nameInfo.nameType == .contact { - showAlert( - NSLocalizedString("Unsupported contact name", comment: "alert title"), - message: NSLocalizedString("Connecting via contact name requires a newer app version.", comment: "alert message") + upgrade - ) - } else { - showAlert( - NSLocalizedString("Unsupported channel name", comment: "alert title"), - message: NSLocalizedString("Connecting via channel name requires a newer app version.", comment: "alert message") + upgrade - ) - } -} - struct IncognitoToggle: View { @EnvironmentObject var theme: AppTheme @Binding var incognitoEnabled: Bool @@ -1145,6 +1132,7 @@ private func showPrepareContactAlert( connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, ownerVerification: OwnerVerification? = nil, + verifiedDomain: SimplexDomain? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1171,7 +1159,7 @@ private func showPrepareContactAlert( onConfirm: { Task { do { - let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData) + let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain) await MainActor.run { ChatModel.shared.addChat(Chat(chat)) openKnownChat(chat.id, dismiss: dismiss, cleanup: cleanup) @@ -1193,6 +1181,7 @@ private func showPrepareGroupAlert( groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, ownerVerification: OwnerVerification? = nil, + verifiedDomain: SimplexDomain? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1221,7 +1210,7 @@ private func showPrepareGroupAlert( onConfirm: { Task { do { - let chat = try await apiPrepareGroup(connLink: connectionLink, directLink: groupShortLinkInfo?.direct ?? true, groupShortLinkData: groupShortLinkData) + let chat = try await apiPrepareGroup(connLink: connectionLink, directLink: groupShortLinkInfo?.direct ?? true, groupShortLinkData: groupShortLinkData, verifiedDomain: verifiedDomain) await MainActor.run { if let relays = groupShortLinkInfo?.groupRelays, !relays.isEmpty, case let .group(gInfo, _) = chat.chatInfo { @@ -1319,10 +1308,6 @@ func planAndConnect( filterKnownGroup: ((GroupInfo) -> Void)? = nil ) { switch strConnectTarget(shortOrFullLink) { - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) - cleanup?() - return case let .link(_, linkType, _): if linkType == .relay { showAlert( @@ -1332,7 +1317,9 @@ func planAndConnect( cleanup?() return } - case .none: break + // A SimplexName falls through to apiConnectPlan, which resolves it on the + // core (the /_connect plan command accepts a name target, not only a link). + case .name, .none: break } ConnectProgressManager.shared.cancelConnectProgress() let inProgress = BoxedValue(true) @@ -1416,7 +1403,7 @@ func planAndConnect( } case let .contactAddress(cap): switch cap { - case let .ok(contactSLinkData_, ownerVerification): + case let .ok(contactSLinkData_, ownerVerification, verifiedDomain): if let contactSLinkData = contactSLinkData_ { logger.debug("planAndConnect, .contactAddress, .ok, short link data present") await MainActor.run { @@ -1424,6 +1411,7 @@ func planAndConnect( connectionLink: connectionLink, contactShortLinkData: contactSLinkData, ownerVerification: ownerVerification, + verifiedDomain: verifiedDomain, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1478,6 +1466,9 @@ func planAndConnect( case let .known(contact): logger.debug("planAndConnect, .contactAddress, .known") await MainActor.run { + if ChatModel.shared.getContactChat(contact.contactId) == nil { + ChatModel.shared.addChat(Chat(chatInfo: .direct(contact: contact))) + } if let f = filterKnownContact { f(contact) } else { @@ -1496,7 +1487,7 @@ func planAndConnect( } case let .groupLink(glp): switch glp { - case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification): + case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification, verifiedDomain): if let groupSLinkData = groupSLinkData_ { logger.debug("planAndConnect, .groupLink, .ok, short link data present") await MainActor.run { @@ -1505,6 +1496,7 @@ func planAndConnect( groupShortLinkInfo: groupShortLinkInfo_, groupShortLinkData: groupSLinkData, ownerVerification: ownerVerification, + verifiedDomain: verifiedDomain, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1557,6 +1549,9 @@ func planAndConnect( case let .known(groupInfo): logger.debug("planAndConnect, .groupLink, .known") await MainActor.run { + if ChatModel.shared.getGroupChat(groupInfo.groupId) == nil { + ChatModel.shared.addChat(Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil))) + } if let f = filterKnownGroup { f(groupInfo) } else { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 24cf088918..8095c0297e 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -111,13 +111,16 @@ struct NetworkAndServers: View { Button("Save servers", action: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) }) .disabled(!serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors)) } footer: { - if let errStr = globalServersError(ss.servers.serverErrors) { - ServersErrorView(errStr: errStr) + let errs = globalServersErrors(ss.servers.serverErrors) + if !errs.isEmpty { + ForEach(errs, id: \.self) { err in + ServersErrorView(errStr: err) + } } else if !ss.servers.serverErrors.isEmpty { ServersErrorView(errStr: NSLocalizedString("Errors in servers configuration.", comment: "servers error")) } - if let warnStr = globalServersWarning(ss.servers.serverWarnings) { - ServersWarningView(warnStr: warnStr) + ForEach(globalServersWarnings(ss.servers.serverWarnings), id: \.self) { warn in + ServersWarningView(warnStr: warn) } } @@ -397,17 +400,12 @@ struct ServersWarningView: View { } } -func globalServersError(_ serverErrors: [UserServersError]) -> String? { - for err in serverErrors { - if let errStr = err.globalError { - return errStr - } - } - return nil +func globalServersErrors(_ serverErrors: [UserServersError]) -> [String] { + serverErrors.compactMap { $0.globalError } } -func globalServersWarning(_ serverWarnings: [UserServersWarning]) -> String? { - for warn in serverWarnings { +func globalServersWarnings(_ serverWarnings: [UserServersWarning]) -> [String] { + serverWarnings.map { warn in switch warn { case let .noChatRelays(user): let text = NSLocalizedString("No chat relays enabled.", comment: "servers warning") @@ -417,9 +415,16 @@ func globalServersWarning(_ serverWarnings: [UserServersWarning]) -> String? { user.localDisplayName ) + " " + text } else { return text } + case let .noNamesServers(user): + let text = NSLocalizedString("No servers to resolve names.", comment: "servers warning") + if let user = user { + return String.localizedStringWithFormat( + NSLocalizedString("For chat profile %@:", comment: "servers warning"), + user.localDisplayName + ) + " " + text + } else { return text } } } - return nil } func bindingForChatRelays(_ userServers: Binding<[UserOperatorServers]>, _ opIndex: Int) -> Binding<[UserChatRelay]> { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index 26f24f2f0f..4e2f1992d6 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -52,10 +52,16 @@ struct OperatorView: View { Text("Operator") .foregroundColor(theme.colors.secondary) } footer: { - if let errStr = globalServersError(serverErrors) { - ServersErrorView(errStr: errStr) - } else if let warnStr = globalServersWarning(serverWarnings) { - ServersWarningView(warnStr: warnStr) + let errs = globalServersErrors(serverErrors) + let warns = globalServersWarnings(serverWarnings) + if !errs.isEmpty { + ForEach(errs, id: \.self) { err in + ServersErrorView(errStr: err) + } + } else if !warns.isEmpty { + ForEach(warns, id: \.self) { warn in + ServersWarningView(warnStr: warn) + } } else { switch (userServers[operatorIndex].operator_.conditionsAcceptance) { case let .accepted(acceptedAt, _): @@ -105,6 +111,10 @@ struct OperatorView: View { .onChange(of: userServers[operatorIndex].operator_.smpRoles.proxy) { _ in validateServers_($userServers, $serverErrors, $serverWarnings) } + Toggle("To resolve names", isOn: $userServers[operatorIndex].operator_.smpRoles.names) + .onChange(of: userServers[operatorIndex].operator_.smpRoles.names) { _ in + validateServers_($userServers, $serverErrors, $serverWarnings) + } } header: { Text("Use for messages") .foregroundColor(theme.colors.secondary) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift index b059be7cb0..a92491edef 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift @@ -169,10 +169,16 @@ struct YourServersView: View { .hidden() } } footer: { - if let errStr = globalServersError(serverErrors) { - ServersErrorView(errStr: errStr) - } else if let warnStr = globalServersWarning(serverWarnings) { - ServersWarningView(warnStr: warnStr) + let errs = globalServersErrors(serverErrors) + let warns = globalServersWarnings(serverWarnings) + if !errs.isEmpty { + ForEach(errs, id: \.self) { err in + ServersErrorView(errStr: err) + } + } else if !warns.isEmpty { + ForEach(warns, id: \.self) { warn in + ServersWarningView(warnStr: warn) + } } } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index ad6b2d4454..f1bacee425 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -16,6 +16,7 @@ struct PrivacySettings: View { @AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS, store: groupDefaults) private var useLinkPreviews = true @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true + @AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) private var verifySimplexNames = false @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true @@ -193,6 +194,9 @@ struct PrivacySettings: View { m.draftChatId = nil } } + settingsRow("number", color: theme.colors.secondary) { + Toggle("Verify SimpleX names", isOn: $verifySimplexNames) + } } header: { Text("Chats") .foregroundColor(theme.colors.secondary) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 135a90c65e..057ce5a227 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -32,6 +32,7 @@ let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_D let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved to app group let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode" let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" +let DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES = "privacyVerifySimplexNames" let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen" let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet" @@ -99,6 +100,7 @@ let appDefaults: [String: Any] = [ DEFAULT_PRIVACY_LINK_PREVIEWS: true, DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue, DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true, + DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES: false, DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true, DEFAULT_PRIVACY_PROTECT_SCREEN: false, DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false, diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index e22042fa24..5d1bea6079 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -191,6 +191,29 @@ struct UserAddressView: View { } } + Section { + NavigationLink { + let simplexName = if let d = chatModel.currentUser?.profile.contactDomain?.shortName { "@\(d)" } else { "" } + SetSimplexDomainView( + title: "Your SimpleX name", + footer: "Let people connect to you via name registered with your SimpleX address.", + prompt: "@yourname.testing", + simplexName: simplexName, + save: { simplexDomain in + do { + let u = try await apiSetUserDomain(simplexDomain) + await MainActor.run { chatModel.updateUser(u) } + return true + } catch { + return false + } + } + ) + } label: { + Label("Your SimpleX name", systemImage: "at") + } + } + Section { createOneTimeLinkButton() } header: { @@ -688,6 +711,59 @@ private func saveAddressSettings(_ settings: AddressSettingsState, _ savedSettin } } +struct SetSimplexDomainView: View { + let title: LocalizedStringKey + let footer: LocalizedStringKey + let prompt: String + @State var simplexName: String + let save: (String?) async -> Bool + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + @State private var saving = false + + var body: some View { + List { + Section { + TextField(prompt, text: $simplexName) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } header: { + Text(verbatim: "") + } footer: { + Text(footer).foregroundColor(theme.colors.secondary) + } + Section { + Button { + saving = true + Task { + let ok = await save(normalized()) + await MainActor.run { + saving = false + if ok { dismiss() } + } + } + } label: { + Text("Save") + } + .disabled(saving) + } + } + .navigationTitle(title) + .navigationBarTitleDisplayMode(.large) + } + + private func normalized() -> String? { + let s = simplexName.trimmingCharacters(in: .whitespacesAndNewlines) + return s.isEmpty + ? nil + : addSimplexTLD(s.hasPrefix("@") || s.hasPrefix("#") ? String(s.dropFirst()) : s) + } + + private func addSimplexTLD(_ d: String) -> String { + if d.contains(".") { d } else { "\(d).simplex" } + } +} + struct UserAddressView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 87e042be82..70606ccc0a 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -183,8 +183,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -563,8 +563,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -735,8 +735,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -822,8 +822,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 5f1d8ef6c2..47522a5deb 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -84,10 +84,8 @@ extension ChatAPIResult { // Spec: spec/api.md#decodeAPIResult public func decodeAPIResult(_ d: Data) -> APIResult { -// print("decodeAPIResult \(String(describing: R.self))") do { -// return try withStackSizeLimit { try jsonDecoder.decode(APIResult.self, from: d) } - return try jsonDecoder.decode(APIResult.self, from: d) + return try withLargeStack { try jsonDecoder.decode(APIResult.self, from: d) } } catch {} if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary { if let (_, jErr) = getOWSF(j, "error") { @@ -106,31 +104,50 @@ public func decodeAPIResult(_ d: Data) -> APIResult { // Default stack size for the main thread is 1mb, for secondary threads - 512 kb. // This function can be used to test what size is used (or to increase available stack size). // Stack size must be a multiple of system page size (16kb). -//private let stackSizeLimit: Int = 256 * 1024 -// -//private func withStackSizeLimit(_ f: @escaping () throws -> T) throws -> T { -// let semaphore = DispatchSemaphore(value: 0) -// var result: Result? -// let thread = Thread { -// do { -// result = .success(try f()) -// } catch { -// result = .failure(error) -// } -// semaphore.signal() -// } -// -// thread.stackSize = stackSizeLimit -// thread.qualityOfService = Thread.current.qualityOfService -// thread.start() -// -// semaphore.wait() -// -// switch result! { -// case let .success(r): return r -// case let .failure(e): throw e -// } -//} +private let stackSizeLimit: Int = 16 * 1024 * 1024 + +private final class LargeStackRunner: NSObject { + static let shared = LargeStackRunner() + + private let serialize = NSLock() // serialize submitters + private let jobReady = DispatchSemaphore(value: 0) + private let jobDone = DispatchSemaphore(value: 0) + private var job: (() -> Void)? + private var thread: Thread? = nil + + private override init() { + super.init() + let t = Thread(target: self, selector: #selector(loop), object: nil) + t.stackSize = stackSizeLimit + t.name = "chat.simplex.decoding" + t.qualityOfService = .default + t.start() + thread = t + } + + @objc private func loop() { + while true { + jobReady.wait() + job?() + jobDone.signal() + } + } + + func run(_ f: @escaping () throws -> T) throws -> T { + serialize.lock() + defer { serialize.unlock() } + var result: Result! + job = { result = Result(catching: f) } + jobReady.signal() + jobDone.wait() + job = nil + return try result.get() + } +} + +func withLargeStack(_ work: @escaping () throws -> T) throws -> T { + try LargeStackRunner.shared.run(work) +} public func parseApiChats(_ jResp: NSDictionary) -> (user: UserRef, chats: [ChatData])? { if let jApiChats = jResp["apiChats"] as? NSDictionary, @@ -167,6 +184,10 @@ public struct CreatedConnLink: Decodable, Hashable { public func simplexChatUri(short: Bool = true) -> String { short ? (connShortLink ?? simplexChatLink(connFullLink)) : simplexChatLink(connFullLink) } + + public var cmdString: String { + connFullLink + (connShortLink.map { " \($0)"} ?? "") + } } public func simplexChatLink(_ uri: String) -> String { @@ -741,6 +762,7 @@ public enum ChatErrorType: Decodable, Hashable { case chatNotStopped case chatStoreChanged case invalidConnReq + case simplexDomainNotReady(simplexDomain: SimplexDomain, simplexDomainError: SimplexDomainError) case unsupportedConnReq case invalidChatMessage(connection: Connection, message: String) case connReqMessageProhibited @@ -896,6 +918,13 @@ public enum AgentErrorType: Decodable, Hashable { case INTERNAL(internalErr: String) case CRITICAL(offerRestart: Bool, criticalErr: String) case INACTIVE + case NO_NAME_SERVERS +} + +public enum NameErrorType: Decodable, Hashable { + case NO_RESOLVER + case NOT_FOUND + case RESOLVER(resolverErr: String) } public enum CommandErrorType: Decodable, Hashable { @@ -937,6 +966,7 @@ public enum ProtocolErrorType: Decodable, Hashable { case LARGE_MSG case EXPIRED case INTERNAL + case NAME(nameErr: NameErrorType) } public enum ProxyError: Decodable, Hashable { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 7a20a8d4ba..3acc2c7eec 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -119,7 +119,8 @@ public struct Profile: Codable, NamedChat, Hashable { image: String? = nil, contactLink: String? = nil, preferences: Preferences? = nil, - peerType: ChatPeerType? = nil + peerType: ChatPeerType? = nil, + contactDomain: SimplexDomainClaim? = nil ) { self.displayName = displayName self.fullName = fullName @@ -127,6 +128,7 @@ public struct Profile: Codable, NamedChat, Hashable { self.image = image self.contactLink = contactLink self.preferences = preferences + self.contactDomain = contactDomain } public var displayName: String @@ -139,6 +141,7 @@ public struct Profile: Codable, NamedChat, Hashable { // the badge proof from the wire profile - opaque to the UI, round-tripped to the core (apiPrepareContact) public var badge: BadgeProof? public var localAlias: String { get { "" } } + public var contactDomain: SimplexDomainClaim? var profileViewName: String { (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" @@ -161,7 +164,9 @@ public struct LocalProfile: Codable, NamedChat, Hashable { preferences: Preferences? = nil, peerType: ChatPeerType? = nil, localBadge: LocalBadge? = nil, - localAlias: String + localAlias: String, + contactDomain: SimplexDomainClaim? = nil, + contactDomainVerified: Bool? = nil ) { self.profileId = profileId self.displayName = displayName @@ -173,6 +178,8 @@ public struct LocalProfile: Codable, NamedChat, Hashable { self.peerType = peerType self.localBadge = localBadge self.localAlias = localAlias + self.contactDomain = contactDomain + self.contactDomainVerified = contactDomainVerified } public var profileId: Int64 @@ -185,6 +192,8 @@ public struct LocalProfile: Codable, NamedChat, Hashable { public var peerType: ChatPeerType? public var localBadge: LocalBadge? public var localAlias: String + public var contactDomain: SimplexDomainClaim? + public var contactDomainVerified: Bool? var profileViewName: String { localAlias == "" @@ -2507,7 +2516,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var groupId: Int64 public var useRelays: Bool public var relayOwnStatus: RelayStatus? = nil - var localDisplayName: GroupName + public var localDisplayName: GroupName public var groupProfile: GroupProfile public var businessChat: BusinessChatInfo? public var fullGroupPreferences: FullGroupPreferences @@ -2534,6 +2543,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var chatTags: [Int64] public var chatItemTTL: Int64? public var localAlias: String + public var groupDomainVerified: Bool? public var isOwner: Bool { return membership.memberRole == .owner && membership.memberCurrent @@ -2614,19 +2624,37 @@ public enum GroupType: Codable, Hashable { } public struct PublicGroupAccess: Codable, Hashable { - public init(groupWebPage: String? = nil, groupDomain: String? = nil, domainWebPage: Bool = false, allowEmbedding: Bool = false) { + public init(groupWebPage: String? = nil, groupDomainClaim: SimplexDomainClaim? = nil, domainWebPage: Bool = false, allowEmbedding: Bool = false) { self.groupWebPage = groupWebPage - self.groupDomain = groupDomain + self.groupDomainClaim = groupDomainClaim self.domainWebPage = domainWebPage self.allowEmbedding = allowEmbedding } public var groupWebPage: String? - public var groupDomain: String? + public var groupDomainClaim: SimplexDomainClaim? public var domainWebPage: Bool = false public var allowEmbedding: Bool = false } +public struct SimplexDomainClaim: Codable, Hashable { + public init(domain: String, proof: SimplexDomainProof? = nil) { + self.domain = domain + self.proof = proof + } + public var domain: String + public var proof: SimplexDomainProof? + + public var shortName: String { + domain.hasSuffix(".simplex") ? String(domain.dropLast(".simplex".count)) : domain + } +} + +public enum SimplexDomainError: Decodable, Hashable { + case noValidLink + case unknownDomain +} + public struct RelayCapabilities: Codable, Hashable { public var webDomain: String? } @@ -5258,28 +5286,60 @@ public enum SimplexLinkType: String, Decodable, Hashable { } } -public struct SimplexNameInfo: Decodable, Equatable, Hashable { +public struct SimplexNameInfo: Codable, Equatable, Hashable { public var nameType: SimplexNameType - public var nameDomain: SimplexNameDomain + public var nameDomain: SimplexDomain + + public init(nameType: SimplexNameType, nameDomain: SimplexDomain) { + self.nameType = nameType + self.nameDomain = nameDomain + } } -public struct SimplexNameDomain: Decodable, Equatable, Hashable { +public struct SimplexDomain: Codable, Equatable, Hashable { public var nameTLD: SimplexTLD public var domain: String public var subDomain: [String] + + // mirrors backend fullDomainName: reverse(subDomain) ++ [domain] ++ tld + public var fullDomainName: String { + let tld: [String] + switch nameTLD { + case .simplex: tld = ["simplex"] + case .testing: tld = ["testing"] + case .web: tld = [] + } + return (subDomain.reversed() + [domain] + tld).joined(separator: ".") + } + + public var cmdString: String { + "domain=\(fullDomainName)" + } + + public init(nameTLD: SimplexTLD, domain: String, subDomain: [String]) { + self.nameTLD = nameTLD + self.domain = domain + self.subDomain = subDomain + } } -public enum SimplexTLD: String, Decodable, Hashable { +public enum SimplexTLD: String, Codable, Hashable { case simplex case testing case web } -public enum SimplexNameType: String, Decodable, Hashable { +public enum SimplexNameType: String, Codable, Hashable { case publicGroup case contact } +public struct SimplexDomainProof: Codable, Hashable { + public var linkOwnerId: String? + public var presHeader: String + public var signature: String +} + public enum FormatColor: String, Decodable, Hashable { case red = "red" case green = "green" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 44361baa73..ef5fe334b8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2039,14 +2039,15 @@ data class Profile( val peerType: ChatPeerType? = null, // the badge proof from the wire profile: not interpreted by the UI (display uses crypto-free LocalBadge), // but preserved so passing a link profile back to the core (apiPrepareContact) keeps the proof - val badge: BadgeProof? = null + val badge: BadgeProof? = null, + val contactDomain: SimplexDomainClaim? = null ): NamedChat { val profileViewName: String get() { return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) + fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain) companion object { val sampleData = Profile( @@ -2068,11 +2069,13 @@ data class LocalProfile( val contactLink: String? = null, val preferences: ChatPreferences? = null, val peerType: ChatPeerType? = null, - val localBadge: LocalBadge? = null + val localBadge: LocalBadge? = null, + val contactDomain: SimplexDomainClaim? = null, + val contactDomainVerified: Boolean? = null ): NamedChat { val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) + fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain) companion object { val sampleData = LocalProfile( @@ -2198,6 +2201,7 @@ data class GroupInfo ( val chatTags: List, val chatItemTTL: Long?, override val localAlias: String, + val groupDomainVerified: Boolean? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" @@ -2319,10 +2323,18 @@ object GroupTypeSerializer : KSerializer { } } +@Serializable +data class SimplexDomainClaim( + val domain: String, + val proof: SimplexDomainProof? = null +) { + val shortName: String get() = domain.removeSuffix(".simplex") +} + @Serializable data class PublicGroupAccess( val groupWebPage: String? = null, - val groupDomain: String? = null, + val groupDomainClaim: SimplexDomainClaim? = null, val domainWebPage: Boolean = false, val allowEmbedding: Boolean = false ) @@ -4874,15 +4886,27 @@ enum class SimplexLinkType(val linkType: String) { @Serializable data class SimplexNameInfo( val nameType: SimplexNameType, - val nameDomain: SimplexNameDomain + val nameDomain: SimplexDomain ) @Serializable -data class SimplexNameDomain( +data class SimplexDomain( val nameTLD: SimplexTLD, val domain: String, val subDomain: List -) +) { + // mirrors backend fullDomainName: reverse(subDomain) + [domain] + tld + val fullDomainName: String get() { + val tld = when (nameTLD) { + SimplexTLD.simplex -> listOf("simplex") + SimplexTLD.testing -> listOf("testing") + SimplexTLD.web -> emptyList() + } + return (subDomain.reversed() + domain + tld).joinToString(".") + } + + val cmdString: String get() = "domain=$fullDomainName" +} @Serializable enum class SimplexTLD { @@ -4897,6 +4921,14 @@ enum class SimplexNameType { @SerialName("contact") contact } +// peer's signed name claim; UI only checks presence +@Serializable +data class SimplexDomainProof( + val linkOwnerId: String? = null, + val presHeader: String, + val signature: String +) + @Serializable enum class FormatColor(val color: String) { red("red"), 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 02353e2c8a..bf0afe4546 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 @@ -122,6 +122,7 @@ class AppPreferences { val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true) val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true) val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true) + val privacyVerifySimplexNames = mkBoolPreference(SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES, false) val privacyLinkPreviewsShowAlert = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, true) val privacySanitizeLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SANITIZE_LINKS, false) // TODO remove @@ -397,6 +398,7 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages" private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" + private const val SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES = "PrivacyVerifySimplexNames" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "PrivacyLinkPreviewsShowAlert" private const val SHARED_PREFS_PRIVACY_SANITIZE_LINKS = "PrivacySanitizeLinks" private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" // TODO remove @@ -1555,6 +1557,46 @@ object ChatController { generalGetString(MR.strings.link_requires_newer_app_version_please_upgrade) ) } + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.SimplexDomainNotReady -> { + val domain = r.err.errorType.simplexDomain.fullDomainName + if (r.err.errorType.simplexDomainError is SimplexDomainError.NoValidLink) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_no_valid_link), + generalGetString(MR.strings.simplex_name_no_valid_link_desc).format(domain) + ) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_unconfirmed), + generalGetString(MR.strings.simplex_name_unconfirmed_desc).format(domain) + ) + } + } + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.NO_NAME_SERVERS -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_error), + generalGetString(MR.strings.simplex_name_no_servers_desc) + ) + } + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.SMP + && r.err.agentError.smpErr is SMPErrorType.NAME -> { + when (val nameErr = r.err.agentError.smpErr.nameErr) { + is NameErrorType.NOT_FOUND -> AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_not_found), + generalGetString(MR.strings.simplex_name_not_found_desc) + ) + is NameErrorType.NO_RESOLVER -> AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_error), + generalGetString(MR.strings.simplex_name_server_no_resolver_desc).format(r.err.agentError.serverAddress) + ) + is NameErrorType.RESOLVER -> AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_error), + generalGetString(MR.strings.simplex_name_resolver_error_desc).format(nameErr.resolverErr) + ) + } + } r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.SMP && r.err.agentError.smpErr is SMPErrorType.AUTH -> { @@ -1587,11 +1629,29 @@ object ChatController { } } + // owner-specific wording for setting one's own/channel name; null for other errors (handled by apiConnectResponseAlert) + fun simplexNameOwnerError(err: ChatError, isChannel: Boolean): String? = + if (err is ChatError.ChatErrorChat && err.errorType is ChatErrorType.SimplexDomainNotReady && err.errorType.simplexDomainError is SimplexDomainError.NoValidLink) { + val domain = err.errorType.simplexDomain.fullDomainName + if (isChannel) generalGetString(MR.strings.simplex_name_owner_no_channel_link).format(domain) + else generalGetString(MR.strings.simplex_name_owner_no_address).format(domain) + } else null + fun connErrorText(e: ChatError): String = when { e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.InvalidConnReq -> generalGetString(MR.strings.invalid_connection_link) e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.UnsupportedConnReq -> generalGetString(MR.strings.unsupported_connection_link) + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.SimplexDomainNotReady -> + if (e.errorType.simplexDomainError is SimplexDomainError.NoValidLink) + generalGetString(MR.strings.simplex_name_no_valid_link) + else generalGetString(MR.strings.simplex_name_unconfirmed) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.NO_NAME_SERVERS -> + generalGetString(MR.strings.simplex_name_error) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.NAME -> + if (e.agentError.smpErr.nameErr is NameErrorType.NOT_FOUND) + generalGetString(MR.strings.simplex_name_not_found) + else generalGetString(MR.strings.simplex_name_error) e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH -> generalGetString(MR.strings.connection_error_auth) e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.BLOCKED -> @@ -1604,18 +1664,18 @@ object ChatController { "${generalGetString(MR.strings.error_prefix)}: ${e.string}" } - suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? { + suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = null): Chat? { val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData)) + val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData, verifiedDomain)) if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh) Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}") return null } - suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData): Chat? { + suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain? = null): Chat? { val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData)) + val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData, verifiedDomain)) if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh) Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}") @@ -1762,6 +1822,38 @@ object ChatController { } } + // name is the encoded SimplexName (e.g. "@alice.simplex"); null clears it. Throws on rejection. + suspend fun apiSetUserDomain(rh: Long?, simplexDomain: String?): User { + val userId = currentUserId("apiSetUserDomain") + val r = sendCmd(rh, CC.ApiSetUserDomain(userId, simplexDomain)) + return when { + r is API.Result && r.res is CR.UserProfileUpdated -> r.res.user.updateRemoteHostId(rh) + r is API.Result && r.res is CR.UserProfileNoChange -> r.res.user.updateRemoteHostId(rh) + else -> { + if (r is API.Error) { + val ownerMsg = simplexNameOwnerError(r.err, isChannel = false) + if (ownerMsg != null) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), ownerMsg) + else apiConnectResponseAlert(r) + } + throw Exception("failed to set SimpleX name: ${r.responseType} ${r.details}") + } + } + } + + suspend fun apiVerifyContactDomain(rh: Long?, contactId: Long): Pair? { + val r = sendCmd(rh, CC.ApiVerifyContactDomain(contactId)) + if (r is API.Result && r.res is CR.ContactDomainVerified) return r.res.contact to r.res.verificationFailure + Log.e(TAG, "apiVerifyContactDomain bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiVerifyGroupDomain(rh: Long?, groupId: Long): Pair? { + val r = sendCmd(rh, CC.ApiVerifyGroupDomain(groupId)) + if (r is API.Result && r.res is CR.GroupDomainVerified) return r.res.groupInfo to r.res.verificationFailure + Log.e(TAG, "apiVerifyGroupDomain bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiSetContactPrefs(rh: Long?, contactId: Long, prefs: ChatPreferences): Contact? { val r = sendCmd(rh, CC.ApiSetContactPrefs(contactId, prefs)) if (r is API.Result && r.res is CR.ContactPrefsUpdated) return r.res.toContact @@ -2289,7 +2381,7 @@ object ChatController { return when { r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup r is API.Error -> { - AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "${r.err.string}") + AlertManager.shared.showAlertMsg(generalGetString(errorTitle), r.err.string) null } else -> { @@ -2303,6 +2395,23 @@ object ChatController { } } + suspend fun apiSetPublicGroupAccess(rh: Long?, groupId: Long, access: PublicGroupAccess): GroupInfo? { + val r = sendCmd(rh, CC.ApiSetPublicGroupAccess(groupId, access)) + return when { + r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup + r is API.Error -> { + val ownerMsg = simplexNameOwnerError(r.err, isChannel = true) + if (ownerMsg != null) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), ownerMsg) + else apiConnectResponseAlert(r) + null + } + else -> { + Log.e(TAG, "apiSetPublicGroupAccess bad response: ${r.responseType} ${r.details}") + null + } + } + } + suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? { val r = sendCmdWithRetry(rh, CC.APICreateGroupLink(groupId, memberRole)) if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink @@ -3705,6 +3814,7 @@ sealed class CC { class ApiLeaveGroup(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC() class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC() + class ApiSetPublicGroupAccess(val groupId: Long, val access: PublicGroupAccess): CC() class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() @@ -3752,8 +3862,8 @@ sealed class CC { class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() class APIConnectPlan(val userId: Long, val connLink: String, val linkOwnerSig: LinkOwnerSig? = null): CC() - class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC() - class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData): CC() + class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData, val verifiedDomain: SimplexDomain? = null): CC() + class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData, val verifiedDomain: SimplexDomain? = null): CC() class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC() class APIChangePreparedGroupUser(val groupId: Long, val newUserId: Long): CC() class APIConnectPreparedContact(val contactId: Long, val incognito: Boolean, val msg: MsgContent?): CC() @@ -3775,6 +3885,9 @@ sealed class CC { class ApiShowMyAddress(val userId: Long): CC() class ApiAddMyAddressShortLink(val userId: Long): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() + class ApiSetUserDomain(val userId: Long, val simplexDomain: String?): CC() + class ApiVerifyContactDomain(val contactId: Long): CC() + class ApiVerifyGroupDomain(val groupId: Long): CC() class ApiSetAddressSettings(val userId: Long, val addressSettings: AddressSettings): CC() class ApiGetCallInvitations: CC() class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC() @@ -3960,8 +4073,8 @@ sealed class CC { val sigStr = if (linkOwnerSig != null) " sig=${json.encodeToString(linkOwnerSig)}" else "" "/_connect plan $userId $connLink$sigStr" } - is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}" - is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} direct=${onOff(directLink)} ${json.encodeToString(groupShortLinkData)}" + is APIPrepareContact -> "/_prepare contact $userId ${connLink.cmdString}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(contactShortLinkData)}" + is APIPrepareGroup -> "/_prepare group $userId ${connLink.cmdString} direct=${onOff(directLink)}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(groupShortLinkData)}" is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId" is APIChangePreparedGroupUser -> "/_set group user #$groupId $newUserId" is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)}${maybeContent(msg)}" @@ -3983,6 +4096,10 @@ sealed class CC { is ApiShowMyAddress -> "/_show_address $userId" is ApiAddMyAddressShortLink -> "/_short_link_address $userId" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" + is ApiSetUserDomain -> "/_set domain $userId" + (if (simplexDomain != null) " $simplexDomain" else "") + is ApiSetPublicGroupAccess -> "/_public group access #$groupId ${json.encodeToString(access)}" + is ApiVerifyContactDomain -> "/_verify domain @$contactId" + is ApiVerifyGroupDomain -> "/_verify domain #$groupId" is ApiSetAddressSettings -> "/_address_settings $userId ${json.encodeToString(addressSettings)}" is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId" is ApiRejectContact -> "/_reject $contactReqId" @@ -4164,6 +4281,10 @@ sealed class CC { is ApiShowMyAddress -> "apiShowMyAddress" is ApiAddMyAddressShortLink -> "apiAddMyAddressShortLink" is ApiSetProfileAddress -> "apiSetProfileAddress" + is ApiSetUserDomain -> "apiSetUserDomain" + is ApiSetPublicGroupAccess -> "apiSetPublicGroupAccess" + is ApiVerifyContactDomain -> "apiVerifyContactDomain" + is ApiVerifyGroupDomain -> "apiVerifyGroupDomain" is ApiSetAddressSettings -> "apiSetAddressSettings" is ApiAcceptContact -> "apiAcceptContact" is ApiRejectContact -> "apiRejectContact" @@ -4443,8 +4564,8 @@ data class ServerOperator( serverDomains = listOf("simplex.im"), conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false), enabled = true, - smpRoles = ServerRoles(storage = true, proxy = true), - xftpRoles = ServerRoles(storage = true, proxy = true) + smpRoles = ServerRoles(storage = true, proxy = true, names = true), + xftpRoles = ServerRoles(storage = true, proxy = true, names = false) ) } @@ -4504,7 +4625,8 @@ data class ServerOperator( @Serializable data class ServerRoles( val storage: Boolean, - val proxy: Boolean + val proxy: Boolean, + val names: Boolean ) @Serializable @@ -4526,8 +4648,8 @@ data class UserOperatorServers( serverDomains = emptyList(), conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false), enabled = false, - smpRoles = ServerRoles(storage = true, proxy = true), - xftpRoles = ServerRoles(storage = true, proxy = true) + smpRoles = ServerRoles(storage = true, proxy = true, names = true), + xftpRoles = ServerRoles(storage = true, proxy = true, names = false) ) companion object { @@ -4613,6 +4735,7 @@ sealed class UserServersError { @Serializable sealed class UserServersWarning { @Serializable @SerialName("noChatRelays") data class NoChatRelays(val user: UserRef? = null): UserServersWarning() + @Serializable @SerialName("noNamesServers") data class NoNamesServers(val user: UserRef? = null): UserServersWarning() val globalWarning: String? get() = when (this) { @@ -4622,6 +4745,12 @@ sealed class UserServersWarning { String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + " " + text } else text } + is NoNamesServers -> { + val text = generalGetString(MR.strings.no_names_servers_enabled) + if (user != null) { + String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + " " + text + } else text + } } } @@ -6462,6 +6591,8 @@ sealed class CR { @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR() @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR() + @Serializable @SerialName("contactDomainVerified") class ContactDomainVerified(val user: UserRef, val contact: Contact, val verificationFailure: String? = null): CR() + @Serializable @SerialName("groupDomainVerified") class GroupDomainVerified(val user: UserRef, val groupInfo: GroupInfo, val verificationFailure: String? = null): CR() @Serializable @SerialName("groupLinkDataUpdated") class GroupLinkDataUpdated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List, val relaysChanged: Boolean): CR() @Serializable @SerialName("groupRelayUpdated") class GroupRelayUpdated(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val groupRelay: GroupRelay): CR() @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR() @@ -6653,6 +6784,8 @@ sealed class CR { is JoinedGroupMember -> "joinedGroupMember" is ConnectedToGroupMember -> "connectedToGroupMember" is GroupUpdated -> "groupUpdated" + is ContactDomainVerified -> "contactDomainVerified" + is GroupDomainVerified -> "groupDomainVerified" is GroupLinkDataUpdated -> "groupLinkDataUpdated" is GroupRelayUpdated -> "groupRelayUpdated" is GroupLinkCreated -> "groupLinkCreated" @@ -6837,6 +6970,8 @@ sealed class CR { is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact") is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) + is ContactDomainVerified -> withUser(user, "contact: ${json.encodeToString(contact)}\nverificationFailure: $verificationFailure") + is GroupDomainVerified -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nverificationFailure: $verificationFailure") is GroupLinkDataUpdated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays\nrelaysChanged: $relaysChanged") is GroupRelayUpdated -> withUser(user, "groupInfo: $groupInfo\nmember: $member\ngroupRelay: $groupRelay") is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink") @@ -6948,6 +7083,8 @@ data class CreatedConnLink(val connFullLink: String, val connShortLink: String?) fun simplexChatUri(short: Boolean): String = if (short) connShortLink ?: simplexChatLink(connFullLink) else simplexChatLink(connFullLink) + + val cmdString: String get() = connFullLink + (if (connShortLink == null) "" else " $connShortLink") } fun simplexChatLink(uri: String): String = @@ -6960,6 +7097,12 @@ sealed class OwnerVerification { @Serializable @SerialName("failed") class Failed(val reason: String) : OwnerVerification() } +@Serializable +sealed class SimplexDomainError { + @Serializable @SerialName("noValidLink") object NoValidLink : SimplexDomainError() + @Serializable @SerialName("unknownDomain") object UnknownDomain : SimplexDomainError() +} + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @@ -6978,7 +7121,7 @@ sealed class InvitationLinkPlan { @Serializable sealed class ContactAddressPlan { - @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): ContactAddressPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null, val verifiedDomain: SimplexDomain? = null): ContactAddressPlan() @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan() @@ -6988,7 +7131,7 @@ sealed class ContactAddressPlan { @Serializable sealed class GroupLinkPlan { - @Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = null): GroupLinkPlan() + @Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = null, val verifiedDomain: SimplexDomain? = null): GroupLinkPlan() @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() @@ -7297,6 +7440,7 @@ sealed class ChatErrorType { is ChatStoreChanged -> "chatStoreChanged" is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" + is SimplexDomainNotReady -> "simplexDomainNotReady" is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" is ConnReqMessageProhibited -> "connReqMessageProhibited" @@ -7379,6 +7523,7 @@ sealed class ChatErrorType { @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() + @Serializable @SerialName("simplexDomainNotReady") class SimplexDomainNotReady(val simplexDomain: SimplexDomain, val simplexDomainError: SimplexDomainError): ChatErrorType() @Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("connReqMessageProhibited") object ConnReqMessageProhibited: ChatErrorType() @@ -7647,6 +7792,7 @@ sealed class AgentErrorType { is INTERNAL -> "INTERNAL $internalErr" is CRITICAL -> "CRITICAL $offerRestart $criticalErr" is INACTIVE -> "INACTIVE" + is NO_NAME_SERVERS -> "NO_NAME_SERVERS" } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType, val errContext: String): AgentErrorType() @@ -7661,6 +7807,19 @@ sealed class AgentErrorType { @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() @Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType() @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() + @Serializable @SerialName("NO_NAME_SERVERS") object NO_NAME_SERVERS: AgentErrorType() +} + +@Serializable +sealed class NameErrorType { + val string: String get() = when (this) { + is NO_RESOLVER -> "NO_RESOLVER" + is NOT_FOUND -> "NOT_FOUND" + is RESOLVER -> "RESOLVER $resolverErr" + } + @Serializable @SerialName("NO_RESOLVER") object NO_RESOLVER: NameErrorType() + @Serializable @SerialName("NOT_FOUND") object NOT_FOUND: NameErrorType() + @Serializable @SerialName("RESOLVER") class RESOLVER(val resolverErr: String): NameErrorType() } @Serializable @@ -7730,6 +7889,7 @@ sealed class SMPErrorType { is LARGE_MSG -> "LARGE_MSG" is EXPIRED -> "EXPIRED" is INTERNAL -> "INTERNAL" + is NAME -> "NAME ${nameErr.string}" } @Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType() @Serializable @SerialName("SESSION") class SESSION: SMPErrorType() @@ -7744,6 +7904,7 @@ sealed class SMPErrorType { @Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType() @Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL: SMPErrorType() + @Serializable @SerialName("NAME") class NAME(val nameErr: NameErrorType): SMPErrorType() } @Serializable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index a099bf333b..e43cc357a0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -757,6 +757,20 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) ) ChatInfoDescription(cInfo, displayName, copyNameToClipboard) + val domain = contact.profile.contactDomain + if (domain != null && (contact.profile.contactDomainVerified != null || domain.proof != null)) { + SimplexNameView( + simplexName = "@${domain.shortName}", + verified = contact.profile.contactDomainVerified, + verify = { + val rhId = chatModel.remoteHostId() + chatModel.controller.apiVerifyContactDomain(rhId, contact.contactId)?.let { (ct, reason) -> + chatModel.chatsContext.updateContact(rhId, ct) + ct.profile.contactDomainVerified to reason + } + } + ) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt new file mode 100644 index 0000000000..ccee6a9035 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt @@ -0,0 +1,94 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.SimplexNameInfo +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* + +// Renders a contact's / channel's SimpleX name with its 3-state verification indicator. +// `verification`: null = not attempted, false = failed, true = verified. +// `verify` runs the verify API, updates the model and returns (newVerification, failureReason); +// null on network error. With `autoVerify`, it runs once on open when state is null. +@Composable +fun SimplexNameView( + simplexName: String, + verified: Boolean?, + verify: suspend () -> Pair? +) { + val scope = rememberCoroutineScope() + val inFlight = remember { mutableStateOf(false) } + val showSpinner = remember { mutableStateOf(false) } + + fun runVerify(manual: Boolean) { + if (inFlight.value) return + inFlight.value = true + scope.launch { + // delay the spinner so a fast result on open doesn't flash it + val spinner = launch { delay(300); if (inFlight.value) showSpinner.value = true } + val res = try { + verify() + } catch (e: Exception) { + Log.e(TAG, "verify SimplexName: ${e.stackTraceToString()}") + null + } + spinner.cancel() + inFlight.value = false + showSpinner.value = false + if (res != null) { + val (newV, reason) = res + // show the reason on a manual run, or on an inconclusive auto run (state stayed null) + if (reason != null && (manual || newV == null)) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.simplex_name_not_verified), reason) + } + } + } + } + + LaunchedEffect(Unit) { + if (chatModel.controller.appPrefs.privacyVerifySimplexNames.get() && verified == null) runVerify(manual = false) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) { + Text( + simplexName, + style = MaterialTheme.typography.body2.copy( + color = if (verified == true) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + fontFamily = if (verified == true) FontFamily.Default else FontFamily.Monospace + ) + ) + when { + showSpinner.value -> + CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp, color = MaterialTheme.colors.secondary) + verified == true -> + Icon(painterResource(MR.images.ic_check_filled), null, Modifier.size(18.dp), tint = MaterialTheme.colors.onBackground) + verified == false -> + Icon( + painterResource(MR.images.ic_close), null, tint = Color.Red, + modifier = Modifier.size(18.dp).clickable { runVerify(manual = true) } + ) + else -> + Text( + stringResource(MR.strings.verify_simplex_name_action), + color = MaterialTheme.colors.primary, + modifier = Modifier.clickable { runVerify(manual = true) } + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt index 262a78b3c8..18a944f671 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt @@ -49,7 +49,7 @@ fun ChannelWebPageView( val trimmedPage = webPage.value.trim() val newAccess = PublicGroupAccess( groupWebPage = trimmedPage.ifEmpty { null }, - groupDomain = access?.groupDomain, + groupDomainClaim = access?.groupDomainClaim, domainWebPage = access?.domainWebPage ?: false, allowEmbedding = allowEmbedding.value ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 2b1c7bcd09..c54ca148d4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -178,6 +178,27 @@ fun ModalData.GroupChatInfoView( manageWebPage = { ModalManager.end.showCustomModal { close -> ChannelWebPageView(rhId, groupInfo, chatModel, close) } }, + setSimplexName = { + ModalManager.end.showCustomModal { close -> + val domain = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName + SetSimplexDomainView( + title = generalGetString(MR.strings.set_simplex_name), + footer = generalGetString(MR.strings.set_channel_simplex_name_footer), + placeholder = "#channelname.testing", + simplexName = if (domain == null) "" else "#$domain", + save = { domain -> + val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?: PublicGroupAccess() + val newAccess = access.copy(groupDomainClaim = domain?.let { SimplexDomainClaim(it) }) + val gInfo = chatModel.controller.apiSetPublicGroupAccess(rhId, groupInfo.groupId, newAccess) + if (gInfo != null) { + withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, gInfo) } + true + } else false + }, + close = close + ) + } + }, onSearchClicked = onSearchClicked, deletingItems = deletingItems ) @@ -510,6 +531,7 @@ fun ModalData.GroupChatInfoLayout( leaveGroup: () -> Unit, manageGroupLink: () -> Unit, manageWebPage: () -> Unit, + setSimplexName: () -> Unit, close: () -> Unit = { ModalManager.closeAllModalsEverywhere()}, onSearchClicked: () -> Unit, deletingItems: State @@ -616,6 +638,12 @@ fun ModalData.GroupChatInfoLayout( if (groupInfo.isOwner && groupLink != null) { anyTopSectionRowShow = true ChannelLinkButton(manageGroupLink) + SettingsActionItem( + painterResource(MR.images.ic_tag), + stringResource(MR.strings.simplex_name), + setSimplexName, + iconColor = MaterialTheme.colors.secondary + ) } else if (channelLink != null) { anyTopSectionRowShow = true ChannelLinkQRCodeSection(channelLink) @@ -945,6 +973,21 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) { modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) ) ChatInfoDescription(cInfo, displayName, copyNameToClipboard) + val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess + val domain = access?.groupDomainClaim?.shortName + if (domain != null && (groupInfo.groupDomainVerified != null || access.groupDomainClaim?.proof != null)) { + SimplexNameView( + simplexName = "#${domain}", + verified = groupInfo.groupDomainVerified, + verify = { + val rhId = chatModel.remoteHostId() + chatModel.controller.apiVerifyGroupDomain(rhId, groupInfo.groupId)?.let { (gInfo, reason) -> + chatModel.chatsContext.updateGroup(rhId, gInfo) + gInfo.groupDomainVerified to reason + } + } + ) + } val webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage if (webPage != null) { val uriHandler = LocalUriHandler.current @@ -1436,6 +1479,7 @@ fun PreviewGroupChatInfoLayout() { manageGroupLink = {}, manageWebPage = {}, onSearchClicked = {}, + setSimplexName = {}, deletingItems = remember { mutableStateOf(true) } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index c9f7d96f39..c7c96cd731 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -338,13 +338,10 @@ fun MarkdownText ( withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) } withAnnotation("SIMPLEX_NAME") { a -> val idx = a.item.toIntOrNull() - val nameInfo = (idx?.let { formattedText.getOrNull(it) }?.format as? Format.SimplexName)?.nameInfo - val (title, msg) = if (nameInfo?.nameType == SimplexNameType.contact) { - generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version) - } else { - generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version) - } - AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}") + val nameText = idx?.let { formattedText.getOrNull(it) }?.text + // The name string is routed through the same connect path as a + // link; planAndConnect resolves it on the core (name target). + if (nameText != null) uriHandler.openVerifiedSimplexUri(nameText) } } if (hasSecrets) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 3012525f9b..99dec506e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -800,7 +800,20 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState searchChatFilteredBySimplexLink.value = null connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) + is ConnectTarget.Name -> { + // A name lookup means "take me to this contact": open the chat if + // it's already known (visible prompt), unlike a pasted link which + // filters the list. So no filterKnownContact here. + hideKeyboard(view) + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + target.text, + close = null, + cleanup = { searchText.value = TextFieldValue() }, + ) + } + } null -> if (!searchShowingSimplexLink.value || it.isEmpty()) { if (it.isNotEmpty()) { focusRequester.requestFocus() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index e5dbe01d68..3776a8d44d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -31,11 +31,6 @@ suspend fun planAndConnect( filterKnownGroup: ((GroupInfo) -> Unit)? = null, ): CompletableDeferred { when (val target = strConnectTarget(shortOrFullLink.trim())) { - is ConnectTarget.Name -> { - showUnsupportedNameAlert(target.nameInfo) - cleanup?.invoke() - return CompletableDeferred(false) - } is ConnectTarget.Link -> { if (target.linkType == SimplexLinkType.relay) { AlertManager.privacySensitive.showAlertMsg( @@ -46,7 +41,9 @@ suspend fun planAndConnect( return CompletableDeferred(false) } } - null -> {} + // A SimplexName falls through to apiConnectPlan, which resolves it on the + // core (the /_connect plan command accepts a name target, not only a link). + is ConnectTarget.Name, null -> {} } connectProgressManager.cancelConnectProgress() val inProgress = mutableStateOf(true) @@ -94,8 +91,8 @@ private suspend fun planAndConnectTask( connectionLink, connectionPlan.invitationLinkPlan.contactSLinkData_, ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification, - close, - cleanup + close = close, + cleanup = cleanup ) } else { Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, no short link data") @@ -157,6 +154,7 @@ private suspend fun planAndConnectTask( connectionLink, connectionPlan.contactAddressPlan.contactSLinkData_, ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, + verifiedDomain = connectionPlan.contactAddressPlan.verifiedDomain, close, cleanup ) @@ -204,6 +202,12 @@ private suspend fun planAndConnectTask( is ContactAddressPlan.Known -> { Log.d(TAG, "planAndConnect, .ContactAddress, .Known") val contact = connectionPlan.contactAddressPlan.contact + // A name-resolved contact is prepared in the store but not yet in the + // chat list (link-prepared chats arrive via NewPreparedChat). Surface it + // so it's visible and openable; no-op if already present. + if (chatModel.getContactChat(contact.contactId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = emptyList())) + } if (filterKnownContact != null) { filterKnownContact(contact) } else { @@ -228,6 +232,7 @@ private suspend fun planAndConnectTask( connectionPlan.groupLinkPlan.groupSLinkInfo_, connectionPlan.groupLinkPlan.groupSLinkData_, ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, + verifiedDomain = connectionPlan.groupLinkPlan.verifiedDomain, close, cleanup ) @@ -288,6 +293,11 @@ private suspend fun planAndConnectTask( is GroupLinkPlan.Known -> { Log.d(TAG, "planAndConnect, .GroupLink, .Known") val groupInfo = connectionPlan.groupLinkPlan.groupInfo + // Same as ContactAddress.Known: surface a name-resolved (prepared) + // group in the chat list so it's visible and openable. + if (chatModel.getGroupChat(groupInfo.groupId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo, groupChatScope = null), chatItems = emptyList())) + } if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { @@ -619,6 +629,7 @@ fun showPrepareContactAlert( connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, ownerVerification: OwnerVerification? = null, + verifiedDomain: SimplexDomain? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -642,7 +653,7 @@ fun showPrepareContactAlert( AlertManager.privacySensitive.hideAlert() ModalManager.closeAllModalsEverywhere() withBGApi { - val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData) + val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData, verifiedDomain) if (chat != null) { withContext(Dispatchers.Main) { ChatController.chatModel.chatsContext.addChat(chat) @@ -664,6 +675,7 @@ fun showPrepareGroupAlert( groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, ownerVerification: OwnerVerification? = null, + verifiedDomain: SimplexDomain? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -686,7 +698,7 @@ fun showPrepareGroupAlert( AlertManager.privacySensitive.hideAlert() withBGApi { val directLink = groupShortLinkInfo?.direct ?: true - val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData) + val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData, verifiedDomain) if (chat != null) { withContext(Dispatchers.Main) { val relays = groupShortLinkInfo?.groupRelays diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 68f42f4186..41df280a8b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -536,7 +536,20 @@ private fun ContactsSearchBar( cleanup = { searchText.value = TextFieldValue() } ) } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) + is ConnectTarget.Name -> { + // A name lookup means "take me to this contact": open the chat if + // it's already known (visible prompt), unlike a pasted link which + // filters the list. So no filterKnownContact here. + hideKeyboard(view) + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + target.text, + close = close, + cleanup = { searchText.value = TextFieldValue() }, + ) + } + } null -> if (!searchShowingSimplexLink.value || it.isEmpty()) { if (it.isNotEmpty()) { focusRequester.requestFocus() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 6799fa1300..d3bca178aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -679,7 +679,11 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState, showQRC showQRCodeScanner.value = false withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } } } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) + is ConnectTarget.Name -> { + pastedLink.value = target.text + showQRCodeScanner.value = false + withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } } + } null -> AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.invalid_contact_link), text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link) @@ -824,7 +828,7 @@ fun strIsSimplexLink(str: String): Boolean { sealed class ConnectTarget { class Link(val text: String, val linkType: SimplexLinkType, val linkText: String) : ConnectTarget() - class Name(val nameInfo: SimplexNameInfo) : ConnectTarget() + class Name(val text: String, val nameInfo: SimplexNameInfo) : ConnectTarget() } fun strConnectTarget(str: String): ConnectTarget? { @@ -835,21 +839,13 @@ fun strConnectTarget(str: String): ConnectTarget? { return ConnectTarget.Link(links[0].text, fmt.linkType, fmt.simplexLinkText) } if (links.isEmpty()) { - val nameInfo = parsedMd.firstNotNullOfOrNull { (it.format as? Format.SimplexName)?.nameInfo } - if (nameInfo != null) return ConnectTarget.Name(nameInfo) + val nameFt = parsedMd.firstOrNull { it.format is Format.SimplexName } + val nameInfo = (nameFt?.format as? Format.SimplexName)?.nameInfo + if (nameFt != null && nameInfo != null) return ConnectTarget.Name(nameFt.text, nameInfo) } return null } -fun showUnsupportedNameAlert(nameInfo: SimplexNameInfo) { - val (title, msg) = if (nameInfo.nameType == SimplexNameType.contact) { - generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version) - } else { - generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version) - } - AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}") -} - @Composable fun IncognitoToggle( incognitoPref: SharedPreference, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 0b698b2c5d..f8bb2d64f9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -134,6 +134,11 @@ fun MorePrivacyView(chatModel: ChatModel) { chatModel.draftChatId.value = null } }) + SettingsPreferenceItem( + painterResource(MR.images.ic_tag), + stringResource(MR.strings.verify_simplex_names), + chatModel.controller.appPrefs.privacyVerifySimplexNames + ) } SectionDividerSpaced() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt new file mode 100644 index 0000000000..c4cf0d1714 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt @@ -0,0 +1,79 @@ +package chat.simplex.common.views.usersettings + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import chat.simplex.common.platform.* +import chat.simplex.common.views.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* + +// Set the user's own (prefix "@") or a channel's (prefix "#") SimpleX name. +// The field is prefilled with the full prefixed name; `save` receives the encoded name (or null to +// clear) and returns true on success (it shows its own error alert otherwise). +@Composable +fun SetSimplexDomainView( + title: String, + footer: String, + placeholder: String, + simplexName: String, + save: suspend (String?) -> Boolean, + close: () -> Unit +) { + val name = rememberSaveable { mutableStateOf(simplexName) } + val saving = remember { mutableStateOf(false) } + val unchanged = name.value.trim() == simplexName.trim() + + fun addSimplexTLD(s: String): String { + return if (s.contains(".")) s else "$s.simplex" + } + + fun normalized(): String? { + val s = name.value.trim() + return when { + s.isEmpty() -> null + s.startsWith("@") || s.startsWith("#") -> addSimplexTLD(s.substring(1)) + else -> addSimplexTLD(s) + } + } + + val doSave: () -> Unit = { + withBGApi { + saving.value = true + val ok = try { save(normalized()) } catch (e: Exception) { + Log.e(TAG, "SetSimplexDomainView save: ${e.stackTraceToString()}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), e.message ?: "") + false + } + saving.value = false + if (ok) withContext(Dispatchers.Main) { close() } + } + } + + ModalView(close = close) { + ColumnWithScrollBar { + AppBarTitle(title) + SectionView { + PlainTextEditor(name, placeholder) + } + SectionTextFooter(footer) + SectionDividerSpaced() + SectionView { + SectionItemView(doSave, disabled = unchanged || saving.value) { + Text( + stringResource(MR.strings.save_verb), + color = if (unchanged || saving.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + SectionBottomSpacer() + } + } +} 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 36c74cceb6..9d0efad152 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 @@ -34,6 +34,8 @@ import chat.simplex.common.views.chat.* import chat.simplex.common.views.newchat.* import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @Composable fun UserAddressView( @@ -360,6 +362,37 @@ private fun UserAddressLayout( SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) } + SectionDividerSpaced() + SectionView { + SettingsActionItem( + painterResource(MR.images.ic_at), + stringResource(MR.strings.your_simplex_name), + click = { + ModalManager.start.showCustomModal { close -> + val domain = user?.profile?.contactDomain?.shortName + SetSimplexDomainView( + title = generalGetString(MR.strings.set_simplex_name), + footer = generalGetString(MR.strings.set_user_simplex_name_footer), + placeholder = "@yourname.testing", + simplexName = if (domain == null) "" else "@$domain", + save = { simplexDomain -> + try { + val u = chatModel.controller.apiSetUserDomain(user?.remoteHostId, simplexDomain) + withContext(Dispatchers.Main) { chatModel.updateUser(u) } + true + } catch (e: Exception) { + Log.e(TAG, "apiSetUserDomain: ${e.message}") + false + } + }, + close = close + ) + } + }, + iconColor = MaterialTheme.colors.secondary + ) + } + SectionDividerSpaced() SectionView(generalGetString(MR.strings.or_to_share_privately)) { CreateOneTimeLinkButton() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index 892a252a9d..556c7270cc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -268,28 +268,32 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { if (currentRemoteHost == null && networkUseSocksProxy.value) { SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) } - val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value) - SectionItemView( - { scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } }, - disabled = saveDisabled, - ) { - Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + SectionDividerSpaced() + SectionView { + val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value) + SectionItemView( + { scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } }, + disabled = saveDisabled, + ) { + Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } } - val serversErr = globalServersError(serverErrors.value) - if (serversErr != null) { - SectionCustomFooter { - ServersErrorFooter(serversErr) + val serversErrs = globalServersErrors(serverErrors.value) + if (serversErrs.isNotEmpty()) { + serversErrs.forEach { err -> + SectionCustomFooter { + ServersErrorFooter(err) + } } } else if (serverErrors.value.isNotEmpty()) { SectionCustomFooter { ServersErrorFooter(generalGetString(MR.strings.errors_in_servers_configuration)) } } - val serversWarn = globalServersWarning(serverWarnings.value) - if (serversWarn != null) { + globalServersWarnings(serverWarnings.value).forEach { warn -> SectionCustomFooter { - ServersWarningFooter(serversWarn) + ServersWarningFooter(warn) } } @@ -951,23 +955,11 @@ fun serversCanBeSaved( return userServers != currUserServers && serverErrors.isEmpty() } -fun globalServersError(serverErrors: List): String? { - for (err in serverErrors) { - if (err.globalError != null) { - return err.globalError - } - } - return null -} +fun globalServersErrors(serverErrors: List): List = + serverErrors.mapNotNull { it.globalError } -fun globalServersWarning(serverWarnings: List): String? { - for (warn in serverWarnings) { - if (warn.globalWarning != null) { - return warn.globalWarning - } - } - return null -} +fun globalServersWarnings(serverWarnings: List): List = + serverWarnings.mapNotNull { it.globalWarning } fun globalSMPServersError(serverErrors: List): String? { for (err in serverErrors) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index f5bceabbf1..53277c9ccd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -211,15 +211,19 @@ fun OperatorViewLayout( rhId = rhId ) } - val serversErr = globalServersError(serverErrors.value) - val serversWarn = globalServersWarning(serverWarnings.value) - if (serversErr != null) { - SectionCustomFooter { - ServersErrorFooter(serversErr) + val serversErrs = globalServersErrors(serverErrors.value) + val serversWarns = globalServersWarnings(serverWarnings.value) + if (serversErrs.isNotEmpty()) { + serversErrs.forEach { err -> + SectionCustomFooter { + ServersErrorFooter(err) + } } - } else if (serversWarn != null) { - SectionCustomFooter { - ServersWarningFooter(serversWarn) + } else if (serversWarns.isNotEmpty()) { + serversWarns.forEach { warn -> + SectionCustomFooter { + ServersWarningFooter(warn) + } } } else { val footerText = when (val c = operator.conditionsAcceptance) { @@ -267,7 +271,7 @@ fun OperatorViewLayout( userServers.value = userServers.value.toMutableList().apply { this[operatorIndex] = this[operatorIndex].copy( operator = this[operatorIndex].operator?.copy( - smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false) + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false, names = false) ) ) } @@ -287,7 +291,27 @@ fun OperatorViewLayout( userServers.value = userServers.value.toMutableList().apply { this[operatorIndex] = this[operatorIndex].copy( operator = this[operatorIndex].operator?.copy( - smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled) + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled, names = false) + ) + ) + } + } + ) + } + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_for_names), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator_.smpRoles.names, + onCheckedChange = { enabled -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy( + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(names = enabled) ?: ServerRoles(storage = false, proxy = false, names = enabled) ) ) } @@ -371,7 +395,7 @@ fun OperatorViewLayout( userServers.value = userServers.value.toMutableList().apply { this[operatorIndex] = this[operatorIndex].copy( operator = this[operatorIndex].operator?.copy( - xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false) + xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false, names = false) ) ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt index 280cd7bedb..63365bd080 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt @@ -185,16 +185,14 @@ fun YourServersViewLayout( iconColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } - val serversErr = globalServersError(serverErrors.value) - if (serversErr != null) { + globalServersErrors(serverErrors.value).forEach { err -> SectionCustomFooter { - ServersErrorFooter(serversErr) + ServersErrorFooter(err) } } - val serversWarn = globalServersWarning(serverWarnings.value) - if (serversWarn != null) { + globalServersWarnings(serverWarnings.value).forEach { warn -> SectionCustomFooter { - ServersWarningFooter(serversWarn) + ServersWarningFooter(warn) } } SectionDividerSpaced() 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 9262bd9dac..96c7226c36 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -146,6 +146,7 @@ For chat profile %s: Errors in servers configuration. No chat relays enabled. + No servers to resolve names. Server warning Error accepting conditions Spam @@ -199,6 +200,16 @@ Connecting via channel name requires a newer app version. Connecting via contact name requires a newer app version. Please upgrade the app. + SimpleX name error + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + Server %1$s does not support name resolution. Configure servers, or use a connection link. + Name not found + This SimpleX name is not registered. Please check the name. + Resolver error: %1$s + No valid link + The SimpleX name %1$s is registered, but it has no valid link. + Unconfirmed name + The SimpleX name %1$s is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. Channel temporarily unavailable Channel has no active relays. Please try to join later. App update required @@ -930,6 +941,17 @@ One-time invitation link 1-time link SimpleX address + Verify name + Verify SimpleX names + SimpleX name not verified + SimpleX name + Your SimpleX name + Set SimpleX name + Error saving name + The SimpleX name %1$s is registered without channel link. Add channel link to the name via the registration page. + The SimpleX name %1$s is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + Let people connect to you via name registered with your SimpleX address. + Let people join via name registered with this channel link. Or show this code Full link Short link @@ -2153,6 +2175,7 @@ Use for messages To receive For private routing + To resolve names Added message servers Use for files To send diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index bedcb87da3..d1467d48fd 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -560,7 +560,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName <> ".\nIt is hidden from the directory until approved." notifyAdminUsers $ "The " <> gt <> " " <> groupRef <> " is updated" <> byMember <> "." sendToApprove g' gr' n' - sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True Nothing) >>= \case Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) -> case dbOwnerMemberId gr of Just ownerGMId -> @@ -818,7 +818,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName forM_ pg_ $ \pg@PublicGroupProfile {groupLink} -> when (groupRegStatus == GRSActive || pendingApproval groupRegStatus) $ do let link = ACL SCMContact $ CLShort groupLink - sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True Nothing) >>= \case Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated = BoolDef updated, linkOwners = ListDef owners}))) -> checkValidOwner dbOwnerMemberId owners $ do when updated $ reapprove pg gr groupRegStatus g' @@ -954,7 +954,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let link = ACL SCMContact $ CLShort connLink mId = MemberId oIdBytes gt' = groupTypeStr gt - sendChatCmd cc (APIConnectPlan userId (Just link) True (Just ownerSig)) >>= \case + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True (Just ownerSig)) >>= \case Right (CRConnectionPlan _ (ACCL SCMContact ccLink) plan) -> handleGroupLinkPlan ct ccLink mId ownerSig gt' plan _ -> sendMessage cc ct "Error: could not connect. Please report it to directory admins." @@ -1000,7 +1000,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let GroupShortLinkData {groupProfile = GroupProfile {displayName}} = groupSLinkData ownerContact = GroupOwnerContact {contactId = contactId' ct, memberId = mId} sendMessage cc ct $ "Joining the " <> gt <> " " <> displayName <> "…" - sendChatCmd cc (APIPrepareGroup userId ccLink False groupSLinkData) >>= \case + sendChatCmd cc (APIPrepareGroup userId ccLink False Nothing groupSLinkData) >>= \case Right (CRNewPreparedChat _ (AChat SCTGroup (Chat (GroupChat gInfo _) _ _))) -> do let gId = groupId' gInfo addGroupReg notifyAdminUsers st cc user ct gInfo GRSProposed $ \_ -> pure () diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index d14435cabd..8dc57dbfc7 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -1363,28 +1363,28 @@ ChatCmdError: Command error (only used in WebSockets API). ### APIConnectPlan -Determine SimpleX link type and if the bot is already connected via this link. +Determine SimpleX link type and if the bot is already connected via this link or name. *Network usage*: interactive. **Parameters**: - userId: int64 -- connectionLink: string? +- connectTarget: string? - resolveKnown: bool - linkOwnerSig: [LinkOwnerSig](./TYPES.md#linkownersig)? **Syntax**: ``` -/_connect plan +/_connect plan ``` ```javascript -'/_connect plan ' + userId + ' ' + connectionLink // JavaScript +'/_connect plan ' + userId + ' ' + connectTarget // JavaScript ``` ```python -'/_connect plan ' + str(userId) + ' ' + connectionLink # Python +'/_connect plan ' + str(userId) + ' ' + connectTarget # Python ``` **Responses**: @@ -1455,26 +1455,26 @@ ChatCmdError: Command error (only used in WebSockets API). ### Connect -Connect via SimpleX link as string in the active user profile. +Connect via SimpleX link or name as string in the active user profile. *Network usage*: interactive. **Parameters**: - incognito: bool -- connLink_: string? +- connTarget_: string? **Syntax**: ``` -/connect[ ] +/connect[ ] ``` ```javascript -'/connect' + (connLink_ ? ' ' + connLink_ : '') // JavaScript +'/connect' + (connTarget_ ? ' ' + connTarget_ : '') // JavaScript ``` ```python -'/connect' + ((' ' + connLink_) if connLink_ is not None else '') # Python +'/connect' + ((' ' + connTarget_) if connTarget_ is not None else '') # Python ``` **Responses**: diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index b38e3f6c6e..d9dfa45961 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -139,6 +139,7 @@ This file is generated automatically. - [MsgReaction](#msgreaction) - [MsgReceiptStatus](#msgreceiptstatus) - [MsgSigStatus](#msgsigstatus) +- [NameErrorType](#nameerrortype) - [NetworkError](#networkerror) - [NewUser](#newuser) - [NoteFolder](#notefolder) @@ -172,8 +173,11 @@ This file is generated automatically. - [SMPAgentError](#smpagenterror) - [SecurityCode](#securitycode) - [SimplePreference](#simplepreference) +- [SimplexDomain](#simplexdomain) +- [SimplexDomainClaim](#simplexdomainclaim) +- [SimplexDomainError](#simplexdomainerror) +- [SimplexDomainProof](#simplexdomainproof) - [SimplexLinkType](#simplexlinktype) -- [SimplexNameDomain](#simplexnamedomain) - [SimplexNameInfo](#simplexnameinfo) - [SimplexNameType](#simplexnametype) - [SimplexTLD](#simplextld) @@ -313,6 +317,9 @@ FILE: - type: "FILE" - fileErr: [FileErrorType](#fileerrortype) +NO_NAME_SERVERS: +- type: "NO_NAME_SERVERS" + PROXY: - type: "PROXY" - proxyServer: string @@ -1104,6 +1111,11 @@ ChatStoreChanged: InvalidConnReq: - type: "invalidConnReq" +SimplexDomainNotReady: +- type: "simplexDomainNotReady" +- simplexDomain: [SimplexDomain](#simplexdomain) +- simplexDomainError: [SimplexDomainError](#simplexdomainerror) + UnsupportedConnReq: - type: "unsupportedConnReq" @@ -1784,6 +1796,7 @@ Ok: - type: "ok" - contactSLinkData_: [ContactShortLinkData](#contactshortlinkdata)? - ownerVerification: [OwnerVerification](#ownerverification)? +- verifiedDomain: [SimplexDomain](#simplexdomain)? OwnLink: - type: "ownLink" @@ -1976,6 +1989,10 @@ EXPIRED: INTERNAL: - type: "INTERNAL" +NAME: +- type: "NAME" +- nameErr: [NameErrorType](#nameerrortype) + DUPLICATE_: - type: "DUPLICATE_" @@ -2330,6 +2347,7 @@ MemberSupport: - membersRequireAttention: int - viaGroupLinkUri: string? - groupKeys: [GroupKeys](#groupkeys)? +- groupDomainVerified: bool? --- @@ -2375,6 +2393,7 @@ Ok: - groupSLinkInfo_: [GroupShortLinkInfo](#groupshortlinkinfo)? - groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? - ownerVerification: [OwnerVerification](#ownerverification)? +- verifiedDomain: [SimplexDomain](#simplexdomain)? OwnLink: - type: "ownLink" @@ -2753,6 +2772,8 @@ Unknown: - peerType: [ChatPeerType](#chatpeertype)? - localBadge: [LocalBadge](#localbadge)? - localAlias: string +- contactDomain: [SimplexDomainClaim](#simplexdomainclaim)? +- contactDomainVerified: bool? --- @@ -2928,6 +2949,23 @@ Unknown: - "signedNoKey" +--- + +## NameErrorType + +**Discriminated union type**: + +NO_RESOLVER: +- type: "NO_RESOLVER" + +NOT_FOUND: +- type: "NOT_FOUND" + +RESOLVER: +- type: "RESOLVER" +- resolverErr: string + + --- ## NetworkError @@ -3098,6 +3136,7 @@ count= - preferences: [Preferences](#preferences)? - peerType: [ChatPeerType](#chatpeertype)? - badge: [BadgeProof](#badgeproof)? +- contactDomain: [SimplexDomainClaim](#simplexdomainclaim)? --- @@ -3146,7 +3185,7 @@ NO_SESSION: **Record type**: - groupWebPage: string? -- groupDomain: string? +- groupDomainClaim: [SimplexDomainClaim](#simplexdomainclaim)? - domainWebPage: bool - allowEmbedding: bool @@ -3530,6 +3569,48 @@ A_QUEUE: - allow: [FeatureAllowed](#featureallowed) +--- + +## SimplexDomain + +**Record type**: +- nameTLD: [SimplexTLD](#simplextld) +- domain: string +- subDomain: [string] + + +--- + +## SimplexDomainClaim + +**Record type**: +- domain: string +- proof: [SimplexDomainProof](#simplexdomainproof)? + + +--- + +## SimplexDomainError + +**Discriminated union type**: + +NoValidLink: +- type: "noValidLink" + +UnknownDomain: +- type: "unknownDomain" + + +--- + +## SimplexDomainProof + +**Record type**: +- linkOwnerId: string? +- presHeader: string +- signature: string + + --- ## SimplexLinkType @@ -3542,23 +3623,13 @@ A_QUEUE: - "relay" ---- - -## SimplexNameDomain - -**Record type**: -- nameTLD: [SimplexTLD](#simplextld) -- domain: string -- subDomain: [string] - - --- ## SimplexNameInfo **Record type**: - nameType: [SimplexNameType](#simplexnametype) -- nameDomain: [SimplexNameDomain](#simplexnamedomain) +- nameDomain: [SimplexDomain](#simplexdomain) --- diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 8ebd510a55..ddfc817fa2 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -135,10 +135,10 @@ chatCommandsDocsData = ( "Connection commands", "These commands may be used to create connections. Most bots do not need to use them - bot users will connect via bot address with auto-accept enabled.", [ ("APIAddContact", [], "Create 1-time invitation link.", ["CRInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False)), - -- `Maybe` in `connectionLink :: Maybe AConnectionLink` is used to signal link parsing error to the runtime (the handler returns CEInvalidConnReq on Nothing); it is NOT API-level optionality. The parameter is required from callers. - ("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectionLink"), + -- `Maybe` in `connectTarget :: Maybe ConnectTarget` is used to signal parse failure to the runtime (the handler returns CEInvalidConnReq on Nothing); it is NOT API-level optionality. The parameter is required from callers. + ("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link or name.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectTarget"), ("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"), - ("Connect", [], "Connect via SimpleX link as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connLink_"), + ("Connect", [], "Connect via SimpleX link or name as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connTarget_"), ("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest", "CRChatCmdError"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"), ("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected", "CRChatCmdError"], [], Nothing, "/_reject " <> Param "contactReqId") ] @@ -410,9 +410,11 @@ undocumentedCommands = "APISetMemberSettings", "APISetNetworkConfig", "APISetNetworkInfo", + "APISetPublicGroupAccess", "APISetServerOperators", "APISetUserContactReceipts", "APISetUserGroupReceipts", + "APISetUserDomain", "APISetUserServers", "APISetUserUIThemes", "APIShareChatMsgContent", @@ -432,7 +434,9 @@ undocumentedCommands = "APIUserRead", "APIValidateServers", "APIVerifyContact", + "APIVerifyContactDomain", "APIVerifyGroupMember", + "APIVerifyGroupDomain", "APIVerifyToken", "CheckChatRunning", "ConfirmRemoteCtrl", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index ddd127241b..f897fc3908 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -148,6 +148,7 @@ undocumentedResponses = "CRContactAliasUpdated", "CRContactCode", "CRContactInfo", + "CRContactDomainVerified", "CRContactRatchetSyncStarted", "CRContactSwitchAborted", "CRContactSwitchStarted", @@ -167,6 +168,7 @@ undocumentedResponses = "CRGroupMemberRatchetSyncStarted", "CRGroupMemberSwitchAborted", "CRGroupMemberSwitchStarted", + "CRGroupDomainVerified", "CRGroupProfile", "CRGroupUserChanged", "CRItemsReadForChat", diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 6313c68838..b28c71d8fd 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -34,7 +34,8 @@ import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Operators import Simplex.Messaging.Agent.Store.Entity (DBStored (..)) -import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), BadgeType (..), JSONBadge (..)) +import Simplex.Chat.Badges +import Simplex.Chat.Names import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared @@ -45,7 +46,7 @@ import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Client import Simplex.Messaging.Crypto.File import Simplex.Messaging.Parsers (dropPrefix, fstToLower) -import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NetworkError (..), ProxyError (..)) +import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NameErrorType (..), NetworkError (..), ProxyError (..)) import Simplex.Messaging.Protocol.Types (ClientNotice (..)) import Simplex.Messaging.Transport import Simplex.RemoteControl.Types @@ -321,6 +322,7 @@ chatTypesDocsData = (sti @MsgReaction, STUnion, "MR", [], "", ""), (sti @MsgReceiptStatus, STEnum, "MR", [], "", ""), (sti @MsgSigStatus, STEnum, "MSS", [], "", ""), + (sti @NameErrorType, STUnion, "", [], "", ""), (sti @NetworkError, STUnion, "NE", [], "", ""), (sti @NewUser, STRecord, "", [], "", ""), (sti @NoteFolder, STRecord, "", [], "", ""), @@ -353,8 +355,11 @@ chatTypesDocsData = (sti @RoleGroupPreference, STRecord, "", [], "", ""), (sti @SecurityCode, STRecord, "", [], "", ""), (sti @SimplePreference, STRecord, "", [], "", ""), + (sti @SimplexDomain, STRecord, "", [], "", ""), + (sti @SimplexDomainClaim, STRecord, "", [], "", ""), + (sti @SimplexDomainError, STUnion, "SDE", [], "", ""), + (sti @SimplexDomainProof, STRecord, "", [], "", ""), (sti @SimplexLinkType, STEnum, "XL", [], "", ""), - (sti @SimplexNameDomain, STRecord, "", [], "", ""), (sti @SimplexNameInfo, STRecord, "", [], "", ""), (sti @SimplexNameType, STEnum, "NT", [], "", ""), (sti @SimplexTLD, STEnum, "TLD", [], "", ""), @@ -548,6 +553,7 @@ deriving instance Generic MsgFilter deriving instance Generic MsgReaction deriving instance Generic MsgReceiptStatus deriving instance Generic MsgSigStatus +deriving instance Generic NameErrorType deriving instance Generic NetworkError deriving instance Generic NewUser deriving instance Generic NoteFolder @@ -578,8 +584,11 @@ deriving instance Generic RelayProfile deriving instance Generic RelayStatus deriving instance Generic ReportReason deriving instance Generic SecurityCode +deriving instance Generic SimplexDomain +deriving instance Generic SimplexDomainClaim +deriving instance Generic SimplexDomainError +deriving instance Generic SimplexDomainProof deriving instance Generic SimplexLinkType -deriving instance Generic SimplexNameDomain deriving instance Generic SimplexNameInfo deriving instance Generic SimplexNameType deriving instance Generic SimplexTLD diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index c5a3b11953..f949a2f92f 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -194,6 +194,7 @@ toTypeInfo tr = primitiveToLower st@(ST t ps) = let t' = fstToLower t in if t' `elem` primitiveTypes then ST t' ps else st stringTypes = [ "AConnectionLink", + "AConnectTarget", "AProtocolType", "AgentConnId", "AgentInvId", @@ -215,11 +216,13 @@ toTypeInfo tr = "Text", "MREmojiChar", "PrivateKey", + "ProofPresHeader", "PublicKey", "ProtocolServer", "SbKey", "SharedMsgId", "Signature", + "StrJSON", "TransportHost", "UIColor", "UserPwd", diff --git a/cabal.project b/cabal.project index 516d029b11..bd8e4ac384 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 98391fd677f547f610a17c48cc96d8c4b2d5e96e + tag: 836254a4c6bd5e17b40acbcaf77a83802d44919e source-repository-package type: git diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index d1b89ffe27..606b09845d 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -495,11 +495,11 @@ export namespace APIAddContact { } } -// Determine SimpleX link type and if the bot is already connected via this link. +// Determine SimpleX link type and if the bot is already connected via this link or name. // Network usage: interactive. export interface APIConnectPlan { userId: number // int64 - connectionLink?: string + connectTarget?: string resolveKnown: boolean linkOwnerSig?: T.LinkOwnerSig } @@ -508,7 +508,7 @@ export namespace APIConnectPlan { export type Response = CR.ConnectionPlan | CR.ChatCmdError export function cmdString(self: APIConnectPlan): string { - return '/_connect plan ' + self.userId + ' ' + self.connectionLink + return '/_connect plan ' + self.userId + ' ' + self.connectTarget } } @@ -528,18 +528,18 @@ export namespace APIConnect { } } -// Connect via SimpleX link as string in the active user profile. +// Connect via SimpleX link or name as string in the active user profile. // Network usage: interactive. export interface Connect { incognito: boolean - connLink_?: string + connTarget_?: string } export namespace Connect { export type Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError export function cmdString(self: Connect): string { - return '/connect' + (self.connLink_ ? ' ' + self.connLink_ : '') + return '/connect' + (self.connTarget_ ? ' ' + self.connTarget_ : '') } } diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 323f174f91..8bd9e7797e 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -66,6 +66,7 @@ export type AgentErrorType = | AgentErrorType.NTF | AgentErrorType.XFTP | AgentErrorType.FILE + | AgentErrorType.NO_NAME_SERVERS | AgentErrorType.PROXY | AgentErrorType.RCP | AgentErrorType.BROKER @@ -84,6 +85,7 @@ export namespace AgentErrorType { | "NTF" | "XFTP" | "FILE" + | "NO_NAME_SERVERS" | "PROXY" | "RCP" | "BROKER" @@ -136,6 +138,10 @@ export namespace AgentErrorType { fileErr: FileErrorType } + export interface NO_NAME_SERVERS extends Interface { + type: "NO_NAME_SERVERS" + } + export interface PROXY extends Interface { type: "PROXY" proxyServer: string @@ -1036,6 +1042,7 @@ export type ChatErrorType = | ChatErrorType.ChatNotStopped | ChatErrorType.ChatStoreChanged | ChatErrorType.InvalidConnReq + | ChatErrorType.SimplexDomainNotReady | ChatErrorType.UnsupportedConnReq | ChatErrorType.ConnReqMessageProhibited | ChatErrorType.ContactNotReady @@ -1113,6 +1120,7 @@ export namespace ChatErrorType { | "chatNotStopped" | "chatStoreChanged" | "invalidConnReq" + | "simplexDomainNotReady" | "unsupportedConnReq" | "connReqMessageProhibited" | "contactNotReady" @@ -1267,6 +1275,12 @@ export namespace ChatErrorType { type: "invalidConnReq" } + export interface SimplexDomainNotReady extends Interface { + type: "simplexDomainNotReady" + simplexDomain: SimplexDomain + simplexDomainError: SimplexDomainError + } + export interface UnsupportedConnReq extends Interface { type: "unsupportedConnReq" } @@ -2035,6 +2049,7 @@ export namespace ContactAddressPlan { type: "ok" contactSLinkData_?: ContactShortLinkData ownerVerification?: OwnerVerification + verifiedDomain?: SimplexDomain } export interface OwnLink extends Interface { @@ -2157,6 +2172,7 @@ export type ErrorType = | ErrorType.LARGE_MSG | ErrorType.EXPIRED | ErrorType.INTERNAL + | ErrorType.NAME | ErrorType.DUPLICATE_ export namespace ErrorType { @@ -2175,6 +2191,7 @@ export namespace ErrorType { | "LARGE_MSG" | "EXPIRED" | "INTERNAL" + | "NAME" | "DUPLICATE_" interface Interface { @@ -2241,6 +2258,11 @@ export namespace ErrorType { type: "INTERNAL" } + export interface NAME extends Interface { + type: "NAME" + nameErr: NameErrorType + } + export interface DUPLICATE_ extends Interface { type: "DUPLICATE_" } @@ -2608,6 +2630,7 @@ export interface GroupInfo { membersRequireAttention: number // int viaGroupLinkUri?: string groupKeys?: GroupKeys + groupDomainVerified?: boolean } export interface GroupKeys { @@ -2658,6 +2681,7 @@ export namespace GroupLinkPlan { groupSLinkInfo_?: GroupShortLinkInfo groupSLinkData_?: GroupShortLinkData ownerVerification?: OwnerVerification + verifiedDomain?: SimplexDomain } export interface OwnLink extends Interface { @@ -2984,6 +3008,8 @@ export interface LocalProfile { peerType?: ChatPeerType localBadge?: LocalBadge localAlias: string + contactDomain?: SimplexDomainClaim + contactDomainVerified?: boolean } export enum MemberCriteria { @@ -3177,6 +3203,29 @@ export enum MsgSigStatus { SignedNoKey = "signedNoKey", } +export type NameErrorType = NameErrorType.NO_RESOLVER | NameErrorType.NOT_FOUND | NameErrorType.RESOLVER + +export namespace NameErrorType { + export type Tag = "NO_RESOLVER" | "NOT_FOUND" | "RESOLVER" + + interface Interface { + type: Tag + } + + export interface NO_RESOLVER extends Interface { + type: "NO_RESOLVER" + } + + export interface NOT_FOUND extends Interface { + type: "NOT_FOUND" + } + + export interface RESOLVER extends Interface { + type: "RESOLVER" + resolverErr: string + } +} + export type NetworkError = | NetworkError.ConnectError | NetworkError.TLSError @@ -3335,6 +3384,7 @@ export interface Profile { preferences?: Preferences peerType?: ChatPeerType badge?: BadgeProof + contactDomain?: SimplexDomainClaim } export type ProxyClientError = @@ -3395,7 +3445,7 @@ export namespace ProxyError { export interface PublicGroupAccess { groupWebPage?: string - groupDomain?: string + groupDomainClaim?: SimplexDomainClaim domainWebPage: boolean allowEmbedding: boolean } @@ -3898,6 +3948,41 @@ export interface SimplePreference { allow: FeatureAllowed } +export interface SimplexDomain { + nameTLD: SimplexTLD + domain: string + subDomain: string[] +} + +export interface SimplexDomainClaim { + domain: string + proof?: SimplexDomainProof +} + +export type SimplexDomainError = SimplexDomainError.NoValidLink | SimplexDomainError.UnknownDomain + +export namespace SimplexDomainError { + export type Tag = "noValidLink" | "unknownDomain" + + interface Interface { + type: Tag + } + + export interface NoValidLink extends Interface { + type: "noValidLink" + } + + export interface UnknownDomain extends Interface { + type: "unknownDomain" + } +} + +export interface SimplexDomainProof { + linkOwnerId?: string + presHeader: string + signature: string +} + export enum SimplexLinkType { Contact = "contact", Invitation = "invitation", @@ -3906,15 +3991,9 @@ export enum SimplexLinkType { Relay = "relay", } -export interface SimplexNameDomain { - nameTLD: SimplexTLD - domain: string - subDomain: string[] -} - export interface SimplexNameInfo { nameType: SimplexNameType - nameDomain: SimplexNameDomain + nameDomain: SimplexDomain } export enum SimplexNameType { diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py index 3847f44811..90f9801b42 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py @@ -434,17 +434,17 @@ def APIAddContact_cmd_string(self: APIAddContact) -> str: APIAddContact_Response = CR.Invitation | CR.ChatCmdError -# Determine SimpleX link type and if the bot is already connected via this link. +# Determine SimpleX link type and if the bot is already connected via this link or name. # Network usage: interactive. class APIConnectPlan(TypedDict): userId: int # int64 - connectionLink: NotRequired[str] + connectTarget: NotRequired[str] resolveKnown: bool linkOwnerSig: NotRequired["T.LinkOwnerSig"] def APIConnectPlan_cmd_string(self: APIConnectPlan) -> str: - return '/_connect plan ' + str(self['userId']) + ' ' + self.get('connectionLink') + return '/_connect plan ' + str(self['userId']) + ' ' + self.get('connectTarget') APIConnectPlan_Response = CR.ConnectionPlan | CR.ChatCmdError @@ -463,15 +463,15 @@ def APIConnect_cmd_string(self: APIConnect) -> str: APIConnect_Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError -# Connect via SimpleX link as string in the active user profile. +# Connect via SimpleX link or name as string in the active user profile. # Network usage: interactive. class Connect(TypedDict): incognito: bool - connLink_: NotRequired[str] + connTarget_: NotRequired[str] def Connect_cmd_string(self: Connect) -> str: - return '/connect' + ((' ' + self.get('connLink_')) if self.get('connLink_') is not None else '') + return '/connect' + ((' ' + self.get('connTarget_')) if self.get('connTarget_') is not None else '') Connect_Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 25825d8f98..4003260ad6 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -78,6 +78,9 @@ class AgentErrorType_FILE(TypedDict): type: Literal["FILE"] fileErr: "FileErrorType" +class AgentErrorType_NO_NAME_SERVERS(TypedDict): + type: Literal["NO_NAME_SERVERS"] + class AgentErrorType_PROXY(TypedDict): type: Literal["PROXY"] proxyServer: str @@ -123,6 +126,7 @@ AgentErrorType = ( | AgentErrorType_NTF | AgentErrorType_XFTP | AgentErrorType_FILE + | AgentErrorType_NO_NAME_SERVERS | AgentErrorType_PROXY | AgentErrorType_RCP | AgentErrorType_BROKER @@ -133,7 +137,7 @@ AgentErrorType = ( | AgentErrorType_INACTIVE ) -AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"] +AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "NO_NAME_SERVERS", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"] class AutoAccept(TypedDict): acceptIncognito: bool @@ -784,6 +788,11 @@ class ChatErrorType_chatStoreChanged(TypedDict): class ChatErrorType_invalidConnReq(TypedDict): type: Literal["invalidConnReq"] +class ChatErrorType_simplexDomainNotReady(TypedDict): + type: Literal["simplexDomainNotReady"] + simplexDomain: "SimplexDomain" + simplexDomainError: "SimplexDomainError" + class ChatErrorType_unsupportedConnReq(TypedDict): type: Literal["unsupportedConnReq"] @@ -1014,6 +1023,7 @@ ChatErrorType = ( | ChatErrorType_chatNotStopped | ChatErrorType_chatStoreChanged | ChatErrorType_invalidConnReq + | ChatErrorType_simplexDomainNotReady | ChatErrorType_unsupportedConnReq | ChatErrorType_connReqMessageProhibited | ChatErrorType_contactNotReady @@ -1070,7 +1080,7 @@ ChatErrorType = ( | ChatErrorType_exception ) -ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"] +ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "simplexDomainNotReady", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"] ChatFeature = Literal["timedMessages", "fullDelete", "reactions", "voice", "files", "calls", "sessions"] @@ -1418,6 +1428,7 @@ class ContactAddressPlan_ok(TypedDict): type: Literal["ok"] contactSLinkData_: NotRequired["ContactShortLinkData"] ownerVerification: NotRequired["OwnerVerification"] + verifiedDomain: NotRequired["SimplexDomain"] class ContactAddressPlan_ownLink(TypedDict): type: Literal["ownLink"] @@ -1553,6 +1564,10 @@ class ErrorType_EXPIRED(TypedDict): class ErrorType_INTERNAL(TypedDict): type: Literal["INTERNAL"] +class ErrorType_NAME(TypedDict): + type: Literal["NAME"] + nameErr: "NameErrorType" + class ErrorType_DUPLICATE_(TypedDict): type: Literal["DUPLICATE_"] @@ -1571,10 +1586,11 @@ ErrorType = ( | ErrorType_LARGE_MSG | ErrorType_EXPIRED | ErrorType_INTERNAL + | ErrorType_NAME | ErrorType_DUPLICATE_ ) -ErrorType_Tag = Literal["BLOCK", "SESSION", "CMD", "PROXY", "AUTH", "BLOCKED", "SERVICE", "CRYPTO", "QUOTA", "STORE", "NO_MSG", "LARGE_MSG", "EXPIRED", "INTERNAL", "DUPLICATE_"] +ErrorType_Tag = Literal["BLOCK", "SESSION", "CMD", "PROXY", "AUTH", "BLOCKED", "SERVICE", "CRYPTO", "QUOTA", "STORE", "NO_MSG", "LARGE_MSG", "EXPIRED", "INTERNAL", "NAME", "DUPLICATE_"] FeatureAllowed = Literal["always", "yes", "no"] @@ -1828,6 +1844,7 @@ class GroupInfo(TypedDict): membersRequireAttention: int # int viaGroupLinkUri: NotRequired[str] groupKeys: NotRequired["GroupKeys"] + groupDomainVerified: NotRequired[bool] class GroupKeys(TypedDict): publicGroupId: str @@ -1851,6 +1868,7 @@ class GroupLinkPlan_ok(TypedDict): groupSLinkInfo_: NotRequired["GroupShortLinkInfo"] groupSLinkData_: NotRequired["GroupShortLinkData"] ownerVerification: NotRequired["OwnerVerification"] + verifiedDomain: NotRequired["SimplexDomain"] class GroupLinkPlan_ownLink(TypedDict): type: Literal["ownLink"] @@ -2089,6 +2107,8 @@ class LocalProfile(TypedDict): peerType: NotRequired["ChatPeerType"] localBadge: NotRequired["LocalBadge"] localAlias: str + contactDomain: NotRequired["SimplexDomainClaim"] + contactDomainVerified: NotRequired[bool] MemberCriteria = Literal["all"] @@ -2221,6 +2241,20 @@ MsgReceiptStatus = Literal["ok", "badMsgHash"] MsgSigStatus = Literal["verified", "signedNoKey"] +class NameErrorType_NO_RESOLVER(TypedDict): + type: Literal["NO_RESOLVER"] + +class NameErrorType_NOT_FOUND(TypedDict): + type: Literal["NOT_FOUND"] + +class NameErrorType_RESOLVER(TypedDict): + type: Literal["RESOLVER"] + resolverErr: str + +NameErrorType = NameErrorType_NO_RESOLVER | NameErrorType_NOT_FOUND | NameErrorType_RESOLVER + +NameErrorType_Tag = Literal["NO_RESOLVER", "NOT_FOUND", "RESOLVER"] + class NetworkError_connectError(TypedDict): type: Literal["connectError"] connectError: str @@ -2340,6 +2374,7 @@ class Profile(TypedDict): preferences: NotRequired["Preferences"] peerType: NotRequired["ChatPeerType"] badge: NotRequired["BadgeProof"] + contactDomain: NotRequired["SimplexDomainClaim"] class ProxyClientError_protocolError(TypedDict): type: Literal["protocolError"] @@ -2381,7 +2416,7 @@ ProxyError_Tag = Literal["PROTOCOL", "BROKER", "BASIC_AUTH", "NO_SESSION"] class PublicGroupAccess(TypedDict): groupWebPage: NotRequired[str] - groupDomain: NotRequired[str] + groupDomainClaim: NotRequired["SimplexDomainClaim"] domainWebPage: bool allowEmbedding: bool @@ -2724,16 +2759,35 @@ class SecurityCode(TypedDict): class SimplePreference(TypedDict): allow: "FeatureAllowed" -SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"] - -class SimplexNameDomain(TypedDict): +class SimplexDomain(TypedDict): nameTLD: "SimplexTLD" domain: str subDomain: list[str] +class SimplexDomainClaim(TypedDict): + domain: str + proof: NotRequired["SimplexDomainProof"] + +class SimplexDomainError_noValidLink(TypedDict): + type: Literal["noValidLink"] + +class SimplexDomainError_unknownDomain(TypedDict): + type: Literal["unknownDomain"] + +SimplexDomainError = SimplexDomainError_noValidLink | SimplexDomainError_unknownDomain + +SimplexDomainError_Tag = Literal["noValidLink", "unknownDomain"] + +class SimplexDomainProof(TypedDict): + linkOwnerId: NotRequired[str] + presHeader: str + signature: str + +SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"] + class SimplexNameInfo(TypedDict): nameType: "SimplexNameType" - nameDomain: "SimplexNameDomain" + nameDomain: "SimplexDomain" SimplexNameType = Literal["publicGroup", "contact"] diff --git a/plans/2026-06-25-name-resolution.md b/plans/2026-06-25-name-resolution.md new file mode 100644 index 0000000000..737de7fe99 --- /dev/null +++ b/plans/2026-06-25-name-resolution.md @@ -0,0 +1,506 @@ +# SimpleX names — simplification plan + +Status: design agreed while reviewing branch `sh/namespace`. This supersedes the +name handling currently on that branch. Line refs are to the working tree at +review time; re-check before editing. + +## 1. Goal + +Reduce the feature to the minimum coherent, secure shape: + +- **One** name per entity, on the **profile** (the entity's claimed identity), + typed `Maybe SimplexNameInfo`. No second "locally-known" copy on the entity row. +- A local **verification status** as a 3-state `Maybe Bool` (not a timestamp): + not-attempted / failed / verified. A failed check never blocks connecting. +- Connect-by-name requires the entity to claim the name, and the result is created as verified. +- The **proof** (address-key signature, context-bound) is **in scope for link + contexts** — connect-by-name, 1-time invitations, contact addresses, channel + join links — so those names are verifiable in this release. Only a name with + **no link context** (a group member, or a name on an `XInfo` profile over an + established connection) is deferred: stored, but not shown until that gap closes. + +Removed from the current branch: the `*.simplex_name` entity columns (the +"locally-known/ct" copy), the `connections.simplex_name` carrier, the four +partial UNIQUE indexes + "newer-claim-wins" clearing, and the `_verified_at` +timestamps. + +## 2. Why (trust model) + +A name resolves to a link via the agent (`resolveSimplexName`); a `NameRecord` +has **lists** of links (`nrSimplexContact`, `nrSimplexChannel`), so +verification matches against **any** of them. "Match any" is safe **only because +the namespace is an on-chain, ENS-style registry**: `nrOwner`/`nrResolver` are +Ethereum addresses (`Names/Record.hs:22-23,36`), so each name has a **single +owner** who sets all its links. An attacker can register *their* name → your +address (the offensive-name case, handled by the claim check below) but **cannot +add a link to your name** — on-chain ownership blocks impersonation. Everything +here depends on that; if names were not single-owner, "match any" would be exploitable. + +The registrant of a name controls what it points to, with no proof that the +target address agreed to it — anyone can publish `@offensive → your address` or +`#offensive → your channel`. Therefore: + +- One-directional verification ("does `resolve(name)` equal a stored link") is + insufficient: it confirms the registrant's assertion, not the address owner's. +- A profile **claiming** a name (and even a link) proves nothing — anyone can + copy a link into a profile. **Control of a link is proven only by an actual + connection/join through it.** +- Sound verification is the intersection: `resolve(name) → link`, **and** the + entity advertises that name in its profile (it claims the name), **and** that link is one + we connected/joined through (control-proven). +- Verifying a name without having connected through its link needs a **signature + by the address key over the name, bound to the presentation context** (§4.8). + This is **in scope** for link contexts: a 1-time invitation includes such a proof + bound to the invite, so resolving the name → address → address key verifies it. + +Consequence: names are verifiable in this release for **channels** (join link), +**contacts connected via their address/name** (the link connected through), and +**1-time-invite contacts** (the in-scope address-key proof). The only case still +deferred is a name with **no link context at all** — a group member, or a name on +an `XInfo` profile over an established connection — which stays stored-but-not-shown. + +## 3. Current branch state (to be changed) + +Migration `M20260603_simplex_name` currently adds, on both SQLite and Postgres: + +- `contacts.simplex_name`, `contacts.simplex_name_verified_at` +- `groups.simplex_name`, `groups.simplex_name_verified_at` +- `connections.simplex_name` +- `contact_profiles.simplex_name`, `group_profiles.simplex_name` +- UNIQUE indexes `idx_contacts_simplex_name`, `idx_groups_simplex_name`, + `idx_contact_profiles_simplex_name`, `idx_group_profiles_simplex_name` +- `server_operators.smp_role_names` (= 1 for `'simplex'`) + +…and threads two names per entity through the types: `Contact.simplexName` / +`GroupInfo.simplexName` (from `*.simplex_name`, "locally known"), and +`LocalProfile.simplexName` (from `*_profiles.simplex_name`, "peer claim"), plus +a `verified_at` timestamp on the entity. `apiVerifySimplexName` verifies the +entity (ct) name against the **profile's self-asserted `contactLink`** for +contacts (wrong link), and against `preparedGroup.connLinkToConnect` for groups +(correct). The `connections.simplex_name` carrier is plumbed through +`createConnection_` but **never written** (all callers pass `Nothing`). + +## 4. Target design + +### 4.1 Name type and JSON encoding + +The name type is **`SimplexNameInfo`** (simplexmq `Simplex/Messaging/SimplexName.hs:37`), +which has: + +- `StrEncoding` (`SimplexName.hs:89`) — canonical `simplex:/name@…` / `#…` string, +- a text `ToField` (`SimplexName.hs:146`) — stores as TEXT, +- a JSON **object** instance: `$(J.deriveJSON defaultJSON ''SimplexNameInfo)` + (`SimplexName.hs:154`) → `{nameType, nameDomain}`. + +**Keep the object JSON** — the UI/API needs the structured form (it reads the name +off `LocalProfile`, `CRSimplexNameVerified`, …). The conflict is only on the +**wire**: `PublicGroupAccess.groupDomain` is a **released** field typed +`Maybe Text` (a JSON string), so the wire form of the name must stay a string. + +Resolve by wrapping **only the wire fields** with simplexmq's generic newtype +`StrJSON` (`Simplex/Messaging/Encoding/String.hs:265`), whose `ToJSON`/`FromJSON` +go through `StrEncoding` → a JSON string (`String.hs:267–272`; idiom +`deriving (ToJSON, FromJSON) via (StrJSON "X" T)`): + +- wire (`Profile`, `GroupProfile`/`PublicGroupAccess`): + `Maybe (StrJSON "SimplexName" SimplexNameInfo)` → JSON **string**, byte-identical + on the wire to the released `Maybe Text`. +- local/UI (`LocalProfile`): `Maybe SimplexNameInfo`, **unwrapped** → JSON **object**. + +Wrap/unwrap at the `Profile` ⇄ `LocalProfile` boundary (`toLocalProfile` / +`fromLocalProfile`). `SimplexNameInfo`'s own JSON instance is untouched, so +`CRSimplexNameVerified` and any other UI-facing use stay object. + +Inherent asymmetry: there is no `LocalGroupProfile`, so the channel name reaches +the UI as a **string** (via `GroupProfile`, which is `StrJSON`), while the contact +name reaches it as an **object** (via `LocalProfile`). If the UI needs the channel +name as an object too, add a decoded field on `GroupInfo` (follow-up). The bot-API +binding generator must render `StrJSON`-wrapped fields as `string`. + +DB stays TEXT: `ToField` (`SimplexName.hs:146`) on write; on read a **hard** +`FromField SimplexNameInfo` (add it — `SimplexName.hs:141-145` says to define it +"when a consumer requires the row-fail behaviour"), so an invalid stored name fails the +row — matching the wire, where a name that won't `strDecode` fails the profile. **No +soft-decode** (`decodeSimplexName` dropped for the name columns). + +### 4.2 Types (field changes) + +- `Profile.contactDomain :: Maybe (StrJSON "SimplexName" SimplexNameInfo)` — NEW; + JSON string (§4.1). The entity's advertised contact name. (Replaces the branch's + `Profile.simplexName`.) +- `Profile.contactDomainProof :: Maybe ClaimProof` — NEW; a flat sibling of + `contactDomain`/`contactLink`, a **wire profile field like `Profile.badge`**. The + **stored** own profile (`contact_profiles`) has the name but **no proof**; the + proof is generated and **added to the outgoing profile at send/save** (§4.8), exactly + as the badge is. `PHSimplexLink` (verifiable) when the profile is saved to a contact + address / 1-time invite; `PHTest` over an established connection (the gap). +- `LocalProfile.contactDomain :: Maybe SimplexNameInfo` (unwrapped → object JSON) + and `LocalProfile.contactDomainVerification :: Maybe Bool` — local status, **not** + on wire `Profile`. `toLocalProfile` unwraps the `StrJSON`; `fromLocalProfile` + re-wraps `contactDomain` and drops the status (mirrors how `localBadge`'s status + is dropped). +- `PublicGroupAccess.groupDomain :: Maybe (StrJSON "SimplexName" SimplexNameInfo)` + — RETYPE from `Maybe Text` (`Types.hs:857`); JSON string, wire-identical to the + released text (§4.1). Owner-set, broadcast in `XGrpInfo`. **Only public, relay-backed + groups** (`groups.use_relays`) can have a name — they have a channel join link to + resolve to and an owner key (`groups.member_priv_key`, `chat_schema.sql:193`) to sign + with; p2p groups have neither. (`contactDomain` on a member is a contact attribute, unaffected.) +- `GroupInfo.groupDomainVerification :: Maybe Bool` — local status, read from the + `groups` table (there is no `LocalGroupProfile`, and `GroupProfile` is the wire + type, so the status cannot be sent with the profile). +- DROP `Contact.simplexName`, `GroupInfo.simplexName`, `Connection.simplexName`, + and both `*VerifiedAt` timestamps. + +Asymmetry, intentional: contact name + status both live on `contact_profiles` +(exposed via `LocalProfile`); the group name lives on `group_profiles` +(exposed via `GroupProfile`/`PublicGroupAccess`) but its status lives on +`groups` (exposed via `GroupInfo`). This is forced — `group_profiles` is the +shared wire profile with no local columns, while `contact_profiles` already +holds local state (`local_alias`). + +### 4.3 Schema — rewrite `M20260603_simplex_name` (branch unreleased) + +Add: +- `contact_profiles`: `contact_domain TEXT`, `contact_domain_verification` + (nullable `INTEGER` SQLite / `SMALLINT` Postgres). +- `groups`: `group_domain_verification` (nullable `INTEGER`/`SMALLINT`). +- `server_operators.smp_role_names` (= 1 for `'simplex'`) — keep. +- `user_contact_links`: the contact-address **root signing key** (BLOB, mirroring + `groups.root_priv_key`) — captured from the 2-step short-link creation — so the + contact-address / 1-time-invite proof can be signed chat-side. (Not present today.) + +No DB change for `group_profiles.group_domain` — it already exists (from +`M20260515_public_group_access`); only the Haskell type and JSON change. + +Remove (vs the current branch migration): `contacts.simplex_name`, +`contacts.simplex_name_verified_at`, `groups.simplex_name`, +`groups.simplex_name_verified_at`, `connections.simplex_name`, +`contact_profiles.simplex_name`, `group_profiles.simplex_name`, and all four +UNIQUE indexes. No name uniqueness is enforced at the DB level — identity comes +from verification, not a constraint. + +Verification column decode: `Maybe BoolInt → Maybe Bool` (`NULL` = not attempted, +`0` = failed, `1` = verified). + +### 4.4 Storage functions (`Store/Shared.hs`, `Store/Direct.hs`, `Store/Groups.hs`) + +- Read the name columns as `Maybe SimplexNameInfo` via the **hard** `FromField` + (§4.1) — an invalid name fails the row; no soft `decodeSimplexName`. +- `toContact` / `toGroupInfo`: read `contact_domain` → `LocalProfile.contactDomain` + and `contact_domain_verification` → `LocalProfile.contactDomainVerification`; + `group_profiles.group_domain` → `PublicGroupAccess.groupDomain` and + `groups.group_domain_verification` → `GroupInfo.groupDomainVerification`. Delete + the ct/cp split and the entity-`simplex_name` reads. +- `createContact_` / `createGroup_` / `createPreparedContact` / `createPreparedGroup`: + set the name on the **profile** columns only; drop the entity-`simplex_name` + argument and the `connections.simplex_name` carrier param on `createConnection_`. +- `updateContactProfile` / `updateGroupProfile`: write `contact_domain` / + `group_domain` from the received profile. Reset the verification to `NULL` + (not-attempted) **only when the name changes**; an XInfo/XGrpInfo with the + **same** name keeps the existing status (a verified name stays verified), exactly as + a badge does. No conflict clearing (no UNIQUE index). +- `getContactBySimplexName` / `getGroupIdBySimplexName`: look up by the **verified** + profile name (the name column joined with verification = `Just True`); on a miss + or unverified, fall through to resolve-and-connect. Consistent with the existing + by-address lookup `getContactViaShortLinkToConnect` (`Direct.hs:963`), which + matches the link with no verification check — a link is the identity, a name is a + claim that only becomes a usable pointer once verified. + +### 4.5 Redaction (`Library/Internal.hs:1246`) + +```haskell +redactedMemberProfile :: GroupInfo -> GroupMember -> Profile -> Profile +redactedMemberProfile g m Profile {…, contactLink, contactDomain} = + let allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g + allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect + in Profile { … + , shortDescr = removeSimplexLink =<< shortDescr -- via allowSimplexLinks + , contactLink = if allowSimplexLinks then contactLink else Nothing + , contactDomain = if allowDirect then contactDomain else Nothing + , contactDomainProof = Nothing } -- member profiles are contextless; never include a proof +``` + +- `allowDirect` is the single primitive (the `DirectMessages` permission); it + controls the name and is reused inside `allowSimplexLinks`. No `allowName` flag, + no second lookup. +- **Behavior changes vs current:** `contactLink` flips from unconditionally + dropped to controlled by `allowSimplexLinks` (a member's contact address becomes + visible whenever links+DMs are allowed — the meaning of "links allowed"); the + name follows the looser `allowDirect`. Rationale: a link is one-tap-to-connect + (low friction), a name only resolves if the recipient deliberately looks it up + (higher friction), so a group can forbid links yet allow name discovery, with + "DMs allowed" the floor for both. +- Signature takes `(GroupInfo, GroupMember)` and derives both flags inside, so + the rule lives in one place. Callers pass `(g, m)`; the own-profile path passes + `(g, membership g)` — behavior-preserving because + `groupFeatureUserAllowed f g ≡ groupFeatureMemberAllowed f (membership g) g` + (both reduce to `groupFeatureMemberAllowed' f (memberRole (membership g)) + (fullGroupPreferences g)`, `Types.hs:646–652`). +- Collapses `groupUserAllowSimplexLinks` (`Types.hs:656`) and the pre-computed + `allowSimplexLinks` wiring at the call sites (`Internal.hs:1244`, + `Subscriber.hs:842,2817,3239`, `Commands.hs:3748,4057`). `Commands.hs:3748` + has `Maybe GroupInfo` → "no group ⇒ no redaction" at the call site. + +### 4.6 Resolution + verification (`Library/Commands.hs`) + +Two cases that differ in whether verification can *fail*: + +**Connect-by-name** (`connectPlanName` / `dispatchResolvedRecord`) — verification is +a **precondition** of connecting. After decoding the resolved short link's embedded +profile, **add the claim check**: require that profile's `contactDomain` / +`groupDomain` to equal the resolved name, else fail with `CESimplexNameNotFound` +("name unknown") and **do not connect**. (The current branch decodes the profile but +never compares the name — this is the missing check.) On success the prepared +contact/group is created with the name on the profile, `connLinkToConnect` = the +resolved link, verification = `Just True` (**created as verified**). There is no "failed" +outcome here — failing to resolve or to claim the name just means no connection. + +**Connected NOT by name** (via an address link or a 1-time link) — the peer's profile +may *claim* a name; it starts **unverified** (`Nothing`). Verification is **post-hoc +and non-blocking**: keep `apiVerifySimplexName` (`/_verify simplex name`). + - **Contacts** verify by a **single path** — check `contactDomainProof` (§4.8): + resolve the claimed name → its link, validate the owner chain and **select the key by + the proof's `linkOwnerId`** (that owner's `ownerKey`, or the root key if `Nothing` — + the usual contact-address case), check the `ClaimProof` signature over + `name <> presHeader`, and check the proof's `presHeader` link == + `preparedContact.connLinkToConnect` (**not** `profile.contactLink` — the branch bug). + Contact addresses and 1-time invites both include the proof. + - **Channels** verify by **presence / link-match**: `resolve(#name)` includes + `preparedGroup.connLinkToConnect` (the join link), whose owner-signed data already + has `groupDomain` (no `ClaimProof` — §4.8). + Result is `Just True` (holds) or `Just False` (fails) — and **`Just False` must NOT + prevent the connection**, exactly as a failed *badge* verification doesn't. **This is + why the status must be 3-state** (`Nothing` not attempted / `Just False` failed / + `Just True` verified). Names that stay `Nothing` have **no proof/link context** — + group members, and names on an `XInfo` profile over an established connection. + +Open option: **auto-verify on connect** (run the same non-blocking check +automatically when connecting via address/1-time link), instead of only on demand. +Either way the status stays 3-state — auto-verification can fail without blocking. + +Keep the pure helpers `firstNameLink` (per-type link pick, cross-type rejection) and +`linksMatch` (scheme-normalized compare). + +### 4.7 Display (`View.hs`) + +A name is shown **when there is a proof to verify**, together with its status — +verified, failed, or not-yet-verified. All three states are shown (a failed or +pending check still shows the name, flagged), so the user sees the name and its +trust level rather than a silent omission. Rendering those three states is **out of +scope for this PR** (UI work), but the core stores the data — the 3-state status and +the proof — to drive it. A name with **no proof to verify** — a group member, or a +name on an `XInfo` profile over an established connection — is **not shown**. + +### 4.8 Proof — a flat profile field, signed by the address key, context-bound + +A name resolves to a **contact address** (the persistent identity link). The proof +asserts "the owner of that address asserts this name", so it is **signed by the +address's key** (the resolved address's own key) — **not** by the +per-connection or 1-time-link key. Every proof is **tied to the link it's shown +through**, so a proof made for one link can't be reused on another; these are +distinct proofs. Store the link in a presentation header: +**rename `BadgePresHeader` (`Badges.hs:212`) → `ProofPresHeader`** (now shared by +badge and name proofs) and add a **`PHSimplexLink AConnShortLink`** constructor. +`AConnShortLink` (`Agent.Protocol.hs:1536`, +`forall m. ConnectionModeI m => ACSL (SConnectionMode m) (ConnShortLink m)`) is the +existing existential, so the context is either a 1-time invitation or a contact +address. The signed payload includes this header, and the **verifier compares the +header's link against the link the proof is presented through** (the current +invite/address) — that comparison is what makes a proof non-replayable across +links. The tag enum (`Badges.hs:200`), the `StrEncoding` (`:216`), and +`badgePresHeaderAccepted` (`:226`, → `proofPresHeaderAccepted`) each gain the new +variant. + +**Type & wire** — a JSON object like `BadgeProof` (`Badges.hs:393`): + + data ClaimProof = ClaimProof + { linkOwnerId :: Maybe OwnerId, -- which owner signed; Nothing = root key (see below) + presHeader :: ProofPresHeader, -- context: PHSimplexLink | PHTest + signature :: C.Signature 'C.Ed25519 -- by that owner's key, over: smpEncode name <> smpEncode presHeader + } + +The signature is by the key of the signer's **owner identity** in the link's owner +chain (`OwnerAuth`, `Agent/Protocol.hs:1829`); `ClaimProof` has +**`linkOwnerId :: Maybe OwnerId`** (`OwnerId`, `:1827`) to name it, so the verifier +checks exactly that key (no iterating the owner list): + +- **Channels** (which can have owners other than the address) sign with the user's **owner key**, + `linkOwnerId = Just oid` — **not** the root key — `groups.member_priv_key` + (`chat_schema.sql:193`). +- A **contact address** has a single owner = its creator, so it signs with the **root + key**, `linkOwnerId = Nothing`. + +The root key (`ShortLinkCreds.linkPrivSigKey`/`linkRootSigKey`, `:1482-83`) otherwise +only *authorizes* owners (`validateOwners`/`validateLinkOwners`, `:1846–1859`); +`Nothing` (root) is **allowed at validation**. Both keys live **chat-side** so signing +is in the chat layer — **but only channels store theirs today** (`groups`); a contact +address (`user_contact_links`, `:386`) has **no key column**, so we must add one +(mirroring `groups.root_priv_key`), captured from the 2-step short-link creation, to +sign the contact-address / 1-time-invite proofs. Signed payload = +`smpEncode name <> smpEncode presHeader` (`name` from the profile's `contactDomain`). +`presHeader` serialises as its `StrEncoding` string, `signature` base64url. + +Per presentation context: + +- **Contact address** (the name's own resolved link): presence in the address link's + owner-signed data already proves the name — but we **include the explicit `ClaimProof` + here too** (bound to the address) so contact verification is + one uniform path (always check the proof) rather than presence-for-addresses / + proof-for-invites. The address can also serve as the **badge** proof's context. +- **1-time invitation**: include a proof **signed by the address key, bound to the + 1-time link** as context. Feasible **now** — the 1-time link is a unique, + single-use context; no general mechanism required. This is what makes + a 1-time-invite contact verifiable. +- **`XInfo` over an established connection**: no link context → the step that adds the proof sets + `PHTest` (unbound) → unverifiable → not shown. Left for later, same as the badge's + `PHTest`. (For **group members** the proof is dropped entirely by redaction, §4.5.) + +Home: **inside the profile** — `Profile.contactDomainProof :: Maybe ClaimProof`, a flat +sibling of `contactDomain`/`contactLink`, exactly like `Profile.badge`. It is **not** +stored on the own profile; like the badge, it is **generated fresh and added to the +outgoing profile at send/save** — when the profile is sent to a peer (`XInfo`) or saved +to a link — by signing `name <> presHeader` with the address root key and setting the +**destination as the `presHeader` context**. Because `presentUserBadge` +(`Internal.hs:2037`) already adds the badge proof to the outgoing profile at *both* +peer-sends and link-data writes, the name-proof step belongs in the **same +function**. The context decides whether a proof is useful: saving to a contact address / +1-time invite sets `PHSimplexLink(that link)` (verifiable — the in-scope cases); an +established-connection peer-send sets `PHTest` (unbound — left for later, same as the +badge). The receiver verifies and stores the 3-state status on +`LocalProfile.contactDomainVerification`, as `localBadge` holds the badge status. +Connect-by-name is **created as verified**. + +**Scope:** the **useful** (`PHSimplexLink`) proofs — set when the profile is saved to +a contact address or 1-time invite — are in this change; the verifier resolves the name → +address → address key → checks the signature and the `presHeader` link. Channels use +presence (below). The contextless established-connection case (a `PHTest` name proof) and +group members are left for later. + +Badges stay **in the profile** — person-scoped, presented per connection +(`presentUserBadge`, `Internal.hs:2037`), including over established connections via +`XInfo`. They share the presentation-header context type with name proofs but sign +with the **badge credential key**, not the address key. Two scopes, two homes. + +The shared step sets **both** proof kinds the same way: `PHSimplexLink(link)` +when the destination is a link (contact address, 1-time invite — verifiable), `PHTest` +over an established connection (unbound — the gap). So name proofs and badges both use +`PHTest` only for the contextless established-connection case. + +No group proof field: a channel is always joined via its **join link** (its own +address), whose owner-signed data already has `groupDomain`, and there is no +"advertised link ≠ resolved address" case for channels — so channels verify by +presence/link-match and need no `ClaimProof`. (If full symmetry is wanted later, add +`GroupProfile.groupDomainProof` the same way.) + +### 4.9 Set-name API (in scope — currently missing) + +Names are **pre-registered out of band** — the app does **not** call RNAME. The API +only **verifies** the name and **adds it to the profile**. The user must be able to +add/change/remove their own name from the UI; the branch has no command for the +**contact** name. + +- **Contact name** — add `APISetUserName :: Maybe SimplexNameInfo -> ChatCommand` + (`Nothing` clears). On set: + 1. Require an **address** — fail if none (the UI won't offer the action without one). + 2. Require it to be a **short link** — if only a long link exists, create the short + link (the name resolves to a short link, and the check below is short-link-based). + 3. Ensure that short link is **in the profile** (`contactLink`) — add it if missing. + 4. **Verify**: resolve the name and compare the short link it points to against the + profile's `contactLink`; fail if they don't match (name not preregistered to this + address). + 5. Set `LocalProfile.contactDomain` and **re-publish the contact-address link data**. + The `ClaimProof` is added when the profile is saved to that link (the send/save + proof-adding step, §4.8) — **not** produced by the API itself. (Steps 1–4 are the verify; + this step is set + re-publish.) + Needs a `ChatCommand` constructor + parser + handler. +- **Channel name** (public, relay-backed groups only) — a **separate** + `APISetPublicGroupName`, parallel to the contact one (not folded into + `SetPublicGroupAccess`): same require-address / require-short-link + / link-in-profile / verify / set-`groupDomain` flow against the channel's **join + link**. Rationale: it mirrors the contact API, and the name's fail-able verify + + preconditions don't mix cleanly with the plain `web=`/`embed=`/`domain_page=` writes. + **Drop `domain=` from `SetPublicGroupAccess`** (`Commands.hs:5461`) so there's a single + verified path; factor out the shared `GroupProfile`-update + `XGrpInfo` broadcast so + both commands reuse it. Verify is **TLD-dependent**: resolve+compare for `TLDSimplex`, + a different/no check for `TLDWeb` (web domains don't resolve through the namespace). +- The own name has **no stored verification status** — the verify step checks it at + add time; the 3-state status is only for peers' names. No RNAME wiring (out of scope). + +## 5. Removal checklist (from the current branch) + +- `Contact.simplexName`, `GroupInfo.simplexName`, `Connection.simplexName`. +- `*VerifiedAt` timestamps (→ `Maybe Bool` status fields). +- `connections.simplex_name` column + the `createConnection_` carrier param + the + `XInfo` carrier consumption in `Subscriber.hs`. +- `contacts.simplex_name`, `groups.simplex_name` columns. +- The four partial UNIQUE indexes + `clearConflictingContactProfileSimplexName_` + / `clearConflictingGroupProfileSimplexName_` + their call sites. +- `getContactBySimplexName` / `getGroupIdBySimplexName` against entity columns + (re-point or remove per 6.b). + +## 6. Resolved decisions + +a. **Wire-string encoding (§4.1):** `SimplexNameInfo` keeps object JSON; wire + fields are wrapped in `StrJSON` (string), `LocalProfile` stays unwrapped + (object). Channel name reaches the UI as a **string** (via `GroupProfile`) and + that is fine — no decoded-object field on `GroupInfo` (no reason to re-connect + to a channel you're in; channel names are only shown verified). The object form + matters for **contacts** (connect-from-groups, sharing), which `LocalProfile` + provides. Residual chore: teach the bot-API binding generator to emit `string` + for `StrJSON` fields and pick the `StrJSON` `name` Symbol. +b. **Lookup (§4.4):** re-point `getContactBySimplexName` (its one caller is + connect-by-name, `Commands.hs:4241`) to the **verified** `contact_profiles. + contact_domain`; miss/unverified ⇒ resolve-and-connect. Keeps the no-network + shortcut for already-known contacts. (`getGroupIdBySimplexName` has no external + caller — drop it.) +c. **Proof (§4.8):** a flat **`Profile.contactDomainProof :: Maybe ClaimProof`**, a wire + profile field like `Profile.badge`. Not stored on the own profile; **generated fresh and + added to the outgoing profile at send/save** (peer `XInfo` or save-to-link) by the + **same function** as the badge (`presentUserBadge`, which already runs at + peer-sends *and* link-data writes). Signed by the signer's **owner-identity key** — a + channel's **owner key** (`groups.member_priv_key`, `linkOwnerId = Just oid`) + or a contact address's **root key** (sole owner, `linkOwnerId = Nothing`) — over + `name <> presHeader`; `linkOwnerId` selects the verification key (`Nothing` = root, + allowed at validation). `PHSimplexLink` for address/invite saves, `PHTest` over + established connections (the gap). Verify checks the signature **and `presHeader`'s link + == the link actually used** (`connLinkToConnect`). Channels use presence (owner-signed + link data). **Gap:** the contact-address key isn't stored chat-side today + (`user_contact_links` has no key column) — add one to sign chat-side. Receiver status + on `LocalProfile.contactDomainVerification`. +d. **`redactedMemberProfile` contactLink exposure (§4.5):** intended — a member's + contact address becomes group-visible when links + DMs are allowed. +e. **Verify command + status (§4.6):** keep `apiVerifySimplexName` — required to + verify a claimed name for entities connected **not** by name (address / 1-time + link). Status is **3-state** because a failed name (or badge) verification must + **not** block the connection; connect-by-name is the only created-as-verified path + (and there a failure to resolve or to claim the name means no connection, not a failed state). + **Auto-verify on connect** (vs. on-demand only) is left open; it doesn't change + the 3-state requirement. + +## 7. Rollout & scope + +The **claim check** (§4.6) — a name resolves only if the **resolved link's own +data claims it** — is the anti-stray-names protection and **must ship regardless**: +a name someone registers against a real address must not "work" without that +address owner's agreement. It needs no signature (the address-case presence suffices), +so it lands with the core change. + +The signed proof (§4.8) can ship in the **same** release (add + verify together) or +be **staged** — implemented at the core level with the user-facing name-addition +hidden in the UI until ready. Either way it stays a *core* change. + +The `ProofPresHeader` rename + `PHSimplexLink` + letting badges opt into the link +context ripples through the badge code — accepted, and bounded: + +- `Badges.hs`: `BadgePresHeader` → `ProofPresHeader`, `BadgePresHeaderTag` → + `ProofPresHeaderTag`, `badgePresHeaderAccepted` → `proofPresHeaderAccepted` + (`:200, :212, :216, :226`); add the `PHSimplexLink AConnShortLink` + tag/constructor/`StrEncoding`/accepted-case; `badgeProof` (`:314`) takes the + renamed type. +- `presentUserBadge` (`Internal.hs:2037`) + its ~15 call sites — the **shared point that adds + proofs to the outgoing profile** (already runs at peer-sends *and* link-data writes): generalize it + to set **both** the badge proof and the name `ClaimProof` onto the outgoing profile, + using `PHSimplexLink link` where the destination is a link, `PHTest` otherwise. + +The pure rename can land as a **standalone prep commit** ahead of the proof; the +`PHSimplexLink` wiring + name proof land with the proof work. diff --git a/plans/2026-06-27-namespace-ui-display-set.md b/plans/2026-06-27-namespace-ui-display-set.md new file mode 100644 index 0000000000..1f01cc4311 --- /dev/null +++ b/plans/2026-06-27-namespace-ui-display-set.md @@ -0,0 +1,243 @@ +# SimpleX name UI: display + verify + set (iOS + Android/desktop) + +Branch: `sh/namespace-ui` (rebased onto core `fc0582cf0` — the finalized verify API). This pass adds +displaying a contact's / channel's SimpleX name with verification state, verifying names (manual + auto), +and two screens for setting the user's own name and a channel's name. + +**Upstream sync (2026-06-27):** core `sh/namespace` is at `5008b4e62` ("refactor setting user name" + +test/comment cleanup, on top of the verify-API split `fc0582cf0`). UI branch rebased onto it (backup tag +`backup-namespace-ui-pre-rebase5`); pure-frontend, rebase clean. All dep-touching changes across these +syncs were **wire-neutral**: cosmetic StrJSON label on `Profile.contactDomain`; `APISetUserName` handler +refactor (constructor `{userId, simplexName}`, `/_set_name` parser, `CRUserProfileUpdated`/`NoChange` +response unchanged); `Internal.hs` record-field reordering in bot profiles; store/view/test cleanup. +`Types.hs` model fields/JSON, verify/set commands, responses, `NameVerifyOutcome`, `NameClaimProof`, and +`SimplexNameInfo` are unchanged. **No UI code change required.** + +## Scope + +Ships (both iOS and Android/desktop): +- Models: decode name / proof / verification fields, add `NameClaimProof` + `SimplexNameInfo` helpers. +- Name display + 3-state verification indicator on contact info and channel info. +- Verify API calls + new response handling. +- "Verify SimpleX names" privacy toggle (default ON). +- Two "Set SimpleX name" screens (own name; channel name). + +Out: no change to the core verify algorithm — the UI only triggers it and renders the result. + +## Decisions + +Single source of truth. The UX walkthrough shows the visuals; the implementation sections give file:line. + +### Product / UX (confirmed) +- **A. Title** — rename to "SimpleX address and name". +- **B. Screen body copy** — placeholders for now (final copy TBD): + - Own: *"Set a SimpleX name so people can connect to you using @yourname instead of a link. The name + must already be registered to your address."* + - Channel: *"Set a SimpleX name so people can find this channel as #name. The name must be registered + to this channel's address."* +- **C. Own name read-only** — No; the current own name appears only inside the set screen. +- **D. Rename scope** — rename the shared string everywhere (settings-menu row + screen title). +- **E. Auto-verify trigger** — with the toggle ON, auto-verify on open only if stored state is `null`; + verified/failed shows the stored result, and tapping the indicator re-verifies. +- **F. Failure reason** — shown on tap of the red cross (alert); an inconclusive result shows a brief + alert on completion. Not shown inline. + +### Technical approach +- **T1. Show the name only when a proof exists** (`contactDomainProof` / `groupDomainProof` != nil). +- **T2. One formatter + one parser.** `SimplexNameInfo.shortName` (display, mirrors `shortNameInfoStr`), + `editDomain` (prefix-less, prefills set fields), `SimplexNameInfo(parsing:)` (decode the encoded + `groupDomain` string). Contact display uses the decoded object; channel display parses `groupDomain`. +- **T3. Set screens send a name string; UI fixes only the type prefix.** `strP` reads `@`->contact / + `#`->group first, so the UI prepends `@` (own) / `#` (channel); the backend canonicalises the domain. + Empty input clears. +- **T4. Channel uses a raw `APIUpdateGroupProfile`** (documented at the call site): core has + `APISetUserName` but no `APISetGroupName`, so the channel name is set by re-sending the cloned + `GroupProfile` with `publicGroup.publicGroupAccess.groupDomain` updated. +- **T5. Gating.** Channel "Set SimpleX name" only when `useRelays && isOwner && publicGroup?.publicGroupAccess != nil`; + own "Set SimpleX name" lives in the existing-address branch (inherently requires an address). + +Minor defaults (flag if wrong): iOS toggle in the first "Chats" Section (PrivacySettings.swift:85); +spinner delay ~300ms; channel name cleared via empty input; set-name fields rely on backend rejection +for validation (+ helper text). + +## UX walkthrough + +Same on both platforms. Nothing existing moves. + +### Name + verification indicator (contact info / channel info) + +A new line under the name, above the description, shown per T1. + +``` + Contact info Channel info + +------------------+ +------------------+ + | [ photo ] | | [ photo ] | + | Alice | | My Team | + | @alice.simplex v| <- new | #myteam Verify | <- new + | "description..."| | "description..."| + +------------------+ +------------------+ + (v = check) (Verify = action) +``` + +Name on the left, indicator on the right; the indicator depends on state: + +| State (`*Verification`) | Name style | Indicator | +|---|---|---| +| Verified (`true`) | accent color | check mark in regular color | +| Failed (`false`) | code style | cross mark in red (tap -> failure reason, per F) | +| Not verified (`null`), toggle OFF | code style | "Verify name" action (accent) | +| Not verified (`null`), toggle ON | code style | auto-verify on open (per E) -> spinner -> result | +| Verifying (in-flight) | code style | delayed spinner (~300ms, so a fast result doesn't flash) | + +### Set screens (2 new buttons -> 2 new screens) + +1. **Own name** — on the "SimpleX address and name" screen, a new "Set SimpleX name" button just above + the "Or to share privately" section: + ``` + [ QR code ] + Share address + Business address (toggle) + Address settings + ------------------ + Set SimpleX name -> <- new + ------------------ + Or to share privately + Create 1-time link + ``` +2. **Channel name** — on channel info, in the existing "Advanced options" section (owners only): + ``` + Advanced options + Web access + Set SimpleX name -> <- new + ``` + +Each opens the same view (parameterised by prefix / body text / save action): explanation + a text field +with a fixed prefix adornment (`@` own -> user types `alice.simplex`; `#` channel -> user types `myteam`) ++ Save. Empty + Save clears the name. + +### Settings toggle +"Verify SimpleX names" toggle, default ON, in the privacy settings "Chats" section (iOS: +PrivacySettings.swift:85, no `MorePrivacyView`; Kotlin: `MorePrivacyView` Chats section, PrivacySettings.kt:118). + +## Backend reference (core @ fc0582cf0) + +Single source for wire/JSON facts. + +- Display: `shortNameInfoStr` (simplexmq Protocol.hs:1594) — public group on default `.simplex` TLD with + empty subdomain -> `#myteam`; else prefix (`@`/`#`) + full domain (`@alice.simplex`, `#myteam.testing`). +- Encoded form: `strEncode SimplexNameInfo = "simplex:/name" <> ("@"|"#") <> fullDomain` (Protocol.hs:1565). +- JSON shapes: `LocalProfile.contactDomain :: Maybe SimplexNameInfo` -> JSON **object**; + `PublicGroupAccess.groupDomain :: Maybe (StrJSON SimplexNameInfo)` -> JSON **string** (encoded form). +- Verification: `LocalProfile.contactDomainVerification` / `GroupInfo.groupDomainVerification :: Maybe Bool` + -> JSON `true`/`false`/absent (decodes as Swift `Bool?` / Kotlin `Boolean?`); null=not attempted, + false=failed, true=verified. +- Proof: `LocalProfile.contactDomainProof` / `PublicGroupAccess.groupDomainProof :: Maybe NameClaimProof` + -> JSON object `{linkOwnerId?: string, presHeader: string, signature: string}` (all strings). +- Verify commands (manual; network/resolver errors are retryable `ChatErrorAgent`): + - `APIVerifyContactName {contactId}` -> `/_verify name @`. + - `APIVerifyPublicGroupName {groupId}` -> `/_verify name #`. + - Outcome `NVOVerified | NVOFailed Text | NVOInconclusive Text` -> persists `Just True` / `Just False` / + leaves unchanged. Responses `CRContactNameVerified {user, contact, verificationResult :: Maybe Text}` / + `CRGroupNameVerified {user, groupInfo, verificationResult :: Maybe Text}` return the **updated** entity + plus `Nothing`=verified / `Just reason`=failure-or-inconclusive text. +- Set own name: `APISetUserName userId (Maybe SimplexNameInfo)` -> `/_set_name []` (parsed by + `strP`, NOT json; Commands.hs:5438). Rejects `name is not registered to your address`. Response + `CRUserProfileUpdated` / `CRUserProfileNoChange`. +- Set channel name: `APIUpdateGroupProfile` (`/_group_profile # `, Commands.hs:5571) with + `groupDomain` set. Rejects `name is not registered to this channel`. Response `CRGroupUpdated`. + +## Gotchas + +1. **iOS Codable (compile-blocking).** `SimplexNameInfo`/`SimplexNameDomain`/`SimplexTLD`/`SimplexNameType` + are `Decodable`-only (ChatTypes.swift:5261-5281); adding `contactDomain` to the `Codable` `LocalProfile` + breaks `Encodable` synthesis -> make all four `Codable`. (Kotlin already `@Serializable`.) +2. **`groupDomain` string carries the `simplex:/name` prefix** -> strip it in `parsing`. +3. **`NameClaimProof` decoded for presence only.** UI just checks `!= nil`; if any field's JSON shape is + uncertain at implementation, decode permissively. +4. **Delayed spinner — no existing helper.** iOS: `@State var verifying` set true after `Task.sleep(~300ms)`, + guarded by an `inFlight` flag. Android: a small `LaunchedEffect(inFlight){ delay(300); show=true }` composable. +5. **Enum JSON parity** — backend `enumJSON (dropPrefix "TLD"/"NT")` lowercases -> matches the Swift/Kotlin + enum raw values. +6. **Navigation.** iOS `NavigationLink`; Android `ModalManager.start.showModalCloseable { ... }` + (template: PrivacySettings.kt:162). + +--- + +## iOS implementation + +### Models — `apps/ios/SimpleXChat/ChatTypes.swift` +- Make `SimplexNameInfo`/`SimplexNameDomain`/`SimplexTLD`/`SimplexNameType` (5261-5281) `Codable`. +- Add `NameClaimProof: Codable, Hashable { presHeader: String; signature: String; linkOwnerId: String? }`. +- `SimplexNameInfo` (5261): add `init?(parsing: String)`, `var shortName: String`, `var editDomain: String`. +- `LocalProfile` (153): add `contactDomain: SimplexNameInfo?`, `contactDomainProof: NameClaimProof?`, + `contactDomainVerification: Bool?`. +- `GroupInfo` (2506): add `groupDomainVerification: Bool?`. +- `PublicGroupAccess` (2616): keep `groupDomain: String?`; add `groupDomainProof: NameClaimProof?`. + +### API — `apps/ios/Shared/Model/{AppAPITypes,SimpleXAPI}.swift` +- `ChatCommand`: `apiSetUserName(userId: Int64, name: String?)` -> `"/_set_name \(userId)" + (name.map{" "+$0} ?? "")`; + `apiVerifyContactName(contactId: Int64)` -> `"/_verify name @\(contactId)"`; + `apiVerifyPublicGroupName(groupId: Int64)` -> `"/_verify name #\(groupId)"`. +- `ChatResponse1`: add `contactNameVerified(user:contact:verificationResult:)` / + `groupNameVerified(user:groupInfo:verificationResult:)` cases (+ `responseType`/`details` entries). + Templates: `contactUpdated` AppAPITypes.swift:1114 / responseType:1193 / details:1267; `groupUpdated`:1140. +- Wrappers: `apiSetUserName(_:)`; `apiVerifyContactName(_:)` (updated `Contact` + reason); + `apiVerifyPublicGroupName(_:)` (updated `GroupInfo` + reason). Channel set reuses `apiUpdateGroup(_:_:)`. + +### Views +- Reusable `SimplexNameView` subview rendering the 5-row state table (incl. delayed spinner + verify action). +- Contact: `ChatInfoView.swift` `contactInfoHeader()` — before the `shortDescr` block (~395), per T1 render + `SimplexNameView` from `contactDomain`/`contactDomainProof`/`contactDomainVerification`; onAppear auto-verify (E). +- Channel: `GroupChatInfoView.swift` `groupInfoHeader()` — before webPage (~334), same from parsed + `groupDomain` + `groupDomainProof` + `groupDomainVerification`; "Set SimpleX name" button in Advanced options (~248). +- Address view: `UserAddressView.swift` — "Set SimpleX name" Section above "Or to share privately" (194). +- New `SetSimplexNameView.swift` (own + channel modes). +- Title rename (A/D): the address screen's title is set by the presenter, not `UserAddressView` + (literal also at UserAddressLearnMore.swift:71) — locate the title source and rename everywhere. +- Toggle: `PrivacySettings.swift` main "Chats" Section (~85) `Toggle("Verify SimpleX names", isOn:)` + bound to `@AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES)`; declare the constant + default `true` in + the `appDefaults` dict (SettingsView.swift:88-105). + +## Android/desktop implementation (multiplatform Kotlin) + +### Models — `model/ChatModel.kt` +- Add `@Serializable data class NameClaimProof(presHeader: String, signature: String, linkOwnerId: String? = null)`. +- `SimplexNameInfo` (4875): add `companion fun parse(encoded): SimplexNameInfo?`, `val shortName`, `val editDomain`. +- `LocalProfile` (2061): add `contactDomain: SimplexNameInfo? = null`, `contactDomainProof: NameClaimProof? = null`, + `contactDomainVerification: Boolean? = null`. +- `GroupInfo` (2181): add `groupDomainVerification: Boolean? = null`. +- `PublicGroupAccess` (2323): keep `groupDomain: String?`; add `groupDomainProof: NameClaimProof? = null`. + +### API — `model/SimpleXAPI.kt` +- `CC`: `ApiSetUserName(userId, name: String?)`, `ApiVerifyContactName(contactId)` -> `"/_verify name @$contactId"`, + `ApiVerifyPublicGroupName(groupId)` -> `"/_verify name #$groupId"` (+ cmdString entries). +- `CR`: `@SerialName("contactNameVerified") ContactNameVerified(user, contact, verificationResult: String?)`, + `GroupNameVerified(user, groupInfo, verificationResult: String?)` (+ `responseType`). Templates: + `ContactUpdated` SimpleXAPI.kt:6454 / responseType:6646; `GroupUpdated`:6500. +- Wrappers `apiSetUserName`, `apiVerifyContactName`, `apiVerifyPublicGroupName`; channel set reuses `apiUpdateGroupProfile`. +- Pref: `val privacyVerifySimplexNames = mkBoolPreference(SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES, true)` (~123). + +### Views +- Reusable `SimplexNameView` composable (the 5-row state table). +- Contact: `views/chat/ChatInfoView.kt` — after `ChatInfoDescription(...)` (~759), per T1; onAppear auto-verify (E). +- Channel: `views/chat/group/GroupChatInfoView.kt` — after `ChatInfoDescription(...)` (~947), per T1; + "Set SimpleX name" button in Advanced options `SectionView` (~805). +- New `views/usersettings/SetSimplexNameView.kt` (own + channel modes). +- `UserAddressView.kt`: "Set SimpleX name" button above the one-time-link section (~347). +- Toggle: `PrivacySettings.kt` `MorePrivacyView` "Chats" Section (~118) `SettingsPreferenceItem(..., appPrefs.privacyVerifySimplexNames)`. +- `strings.xml`: `set_simplex_name`, `verify_simplex_name`, `verify_simplex_names`, screen titles/body; + rename `simplex_address` -> "SimpleX address and name" (D). + +## Build / verify +- iOS: Xcode build of SimpleXChat + app (or swift build of the package targets). +- Android/desktop: `./gradlew` compile of `:common` (desktop target fastest). +- Manual: a peer with a verified name shows accent + check; tampered/failed shows red cross + reason on tap; + toggle OFF shows "Verify name"; toggle ON auto-verifies on open with a delayed spinner. Set own/channel + name; clearing works; core rejection surfaces as an alert. + +## Commit plan (conventional) +1. `feat(names): decode name/proof/verification; add SimplexNameInfo helpers + NameClaimProof` (models, both) +2. `feat(names): verify API (contact/group) + response handling` (CC/CR, wrappers) +3. `feat(names): show name + verification state on contact and channel info` (SimplexNameView + headers) +4. `feat(names): "Verify SimpleX names" privacy toggle + auto-verify on open` +5. `feat(names): set user and channel SimpleX name screens` diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8dca57a258..4ca9cefc7a 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."98391fd677f547f610a17c48cc96d8c4b2d5e96e" = "1ajgidba08dq8yj7xyr9i47rfrlkzbr75j5nyyjcv0zf778sgv0m"; + "https://github.com/simplex-chat/simplexmq.git"."836254a4c6bd5e17b40acbcaf77a83802d44919e" = "1g4385sv7i1h63yk3ch2kw8hprak1dzvr34v9mn6xz0fy10g8xlg"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f8c599c80b..cb36e989da 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -40,6 +40,7 @@ library Simplex.Chat.AppSettings Simplex.Chat.Badges Simplex.Chat.Badges.CLI + Simplex.Chat.Names Simplex.Chat.Call Simplex.Chat.Controller Simplex.Chat.Delivery @@ -144,6 +145,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster + Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name else exposed-modules: Simplex.Chat.Archive @@ -306,6 +308,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster + Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name other-modules: Paths_simplex_chat hs-source-dirs: @@ -579,12 +582,14 @@ test-suite simplex-chat-test ChatTests.Forward ChatTests.Groups ChatTests.Local + ChatTests.Names ChatTests.Profiles ChatTests.Utils JSONFixtures JSONTests MarkdownTests MemberRelationsTests + NameResolver MessageBatching OperatorTests ProtocolTests @@ -665,6 +670,8 @@ test-suite simplex-chat-test , unicode-transforms ==0.4.* , unliftio ==0.2.* , uri-bytestring >=0.3.3.1 && <0.4 + , wai ==3.2.* + , warp ==3.3.* default-language: Haskell2010 if flag(client_postgres) other-modules: diff --git a/src/Simplex/Chat/Badges.hs b/src/Simplex/Chat/Badges.hs index e861d27f11..6e35517ed3 100644 --- a/src/Simplex/Chat/Badges.hs +++ b/src/Simplex/Chat/Badges.hs @@ -1,6 +1,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleInstances #-} @@ -27,8 +28,8 @@ module Simplex.Chat.Badges maxXFTPFileSize, maxFileSizeSupporter, maxFileSizeLegend, - BadgePresHeaderTag (..), - BadgePresHeader (..), + ProofPresHeaderTag (..), + ProofPresHeader (..), BadgePurchase (..), BadgeMasterKey (..), BadgeRequest (..), @@ -197,9 +198,9 @@ maxXFTPFileSize = \case -- presentation, not bound to any context; the 'T' tag marks it so master rejects it. -- PHUnknown is the forward-compat catch-all for tags this version does not interpret. -data BadgePresHeaderTag = PHTestTag | PHUnknownTag Char +data ProofPresHeaderTag = PHTestTag | PHUnknownTag Char -instance StrEncoding BadgePresHeaderTag where +instance StrEncoding ProofPresHeaderTag where strEncode = B.singleton . \case PHTestTag -> 'T' PHUnknownTag c -> c @@ -209,11 +210,13 @@ instance StrEncoding BadgePresHeaderTag where 'T' -> PHTestTag c -> PHUnknownTag c -data BadgePresHeader +data ProofPresHeader = PHTest ByteString | PHUnknown Char ByteString + deriving (Eq, Show) + deriving (ToJSON, FromJSON) via (StrJSON "ProofPresHeader" ProofPresHeader) -instance StrEncoding BadgePresHeader where +instance StrEncoding ProofPresHeader where strEncode = \case PHTest nonce -> strEncode PHTestTag <> nonce PHUnknown c b -> strEncode (PHUnknownTag c) <> b @@ -223,8 +226,8 @@ instance StrEncoding BadgePresHeader where PHUnknownTag c -> PHUnknown c <$> A.takeByteString -- v6.5.x accepts both; v7 will reject PHTest/PHUnknown -badgePresHeaderAccepted :: BadgePresHeader -> Bool -badgePresHeaderAccepted = \case +proofPresHeaderAccepted :: ProofPresHeader -> Bool +proofPresHeaderAccepted = \case PHTest _ -> True PHUnknown _ _ -> True @@ -311,7 +314,7 @@ generateBadgeProof pk (BadgeCredential keyIdx masterKey signature badgeInfo) ph fmap (\p -> BadgeProof keyIdx ph p badgeInfo) <$> bbsProofGen pk signature bbsBadgeHeader ph bbsBadgeDisclosedIndexes (badgeMessages masterKey badgeInfo) -- application-level proof generation with a semantic presentation header -badgeProof :: BBSPublicKey -> BadgeCredential -> BadgePresHeader -> IO (Either String BadgeProof) +badgeProof :: BBSPublicKey -> BadgeCredential -> ProofPresHeader -> IO (Either String BadgeProof) badgeProof pk cred ph = generateBadgeProof pk cred (BBSPresHeader $ strEncode ph) -- Recipient-side: verify a badge proof with the configured key its index points to. @@ -324,7 +327,7 @@ verifyBadge keys b@(BadgeProof keyIdx _ _ _) = case M.lookup keyIdx keys of verifyBadgeWith :: BBSPublicKey -> BadgeProof -> IO Bool verifyBadgeWith pk (BadgeProof _ ph@(BBSPresHeader phBytes) proof badgeInfo) - | either (const False) badgePresHeaderAccepted (strDecode phBytes) = + | either (const False) proofPresHeaderAccepted (strDecode phBytes) = bbsProofVerify pk proof bbsBadgeHeader ph bbsBadgeDisclosedIndexes bbsBadgeMessageCount (badgeInfoMessages badgeInfo) | otherwise = pure False diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 48913af9a5..50b862758b 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -417,6 +417,7 @@ data ChatCommand | APIGetCallInvitations | APICallStatus ContactId WebRTCCallStatus | APIUpdateProfile {userId :: UserId, profile :: Profile} + | APISetUserDomain {userId :: UserId, simplexDomain :: Maybe SimplexDomain} | APISetContactPrefs {contactId :: ContactId, preferences :: Preferences} | APISetContactAlias {contactId :: ContactId, localAlias :: LocalAlias} | APISetGroupAlias {groupId :: GroupId, localAlias :: LocalAlias} @@ -442,6 +443,7 @@ data ChatCommand | APILeaveGroup {groupId :: GroupId} | APIListMembers {groupId :: GroupId} | APIUpdateGroupProfile {groupId :: GroupId, groupProfile :: GroupProfile} + | APISetPublicGroupAccess GroupId PublicGroupAccess | APICreateGroupLink {groupId :: GroupId, memberRole :: GroupMemberRole} | APIGroupLinkMemberRole {groupId :: GroupId, memberRole :: GroupMemberRole} | APIDeleteGroupLink {groupId :: GroupId} @@ -527,15 +529,17 @@ data ChatCommand | AddContact IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to - | APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectionLink is used to report link parsing failure as special error - | APIPrepareContact UserId ACreatedConnLink ContactShortLinkData - | APIPrepareGroup UserId CreatedLinkContact DirectLink GroupShortLinkData + | APIConnectPlan {userId :: UserId, connectTarget :: Maybe AConnectTarget, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectTarget is used to report parsing failure as special error + | APIPrepareContact UserId ACreatedConnLink (Maybe SimplexDomain) ContactShortLinkData + | APIPrepareGroup UserId CreatedLinkContact DirectLink (Maybe SimplexDomain) GroupShortLinkData | APIChangePreparedContactUser ContactId UserId | APIChangePreparedGroupUser GroupId UserId | APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent} | APIConnectPreparedGroup {groupId :: GroupId, incognito :: IncognitoEnabled, ownerContact :: Maybe GroupOwnerContact, msgContent_ :: Maybe MsgContent} | APIConnect {userId :: UserId, incognito :: IncognitoEnabled, preparedLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error - | Connect {incognito :: IncognitoEnabled, connLink_ :: Maybe AConnectionLink} + | Connect {incognito :: IncognitoEnabled, connTarget_ :: Maybe AConnectTarget} + | APIVerifyContactDomain {contactId :: ContactId} + | APIVerifyGroupDomain {groupId :: GroupId} | APIConnectContactViaAddress UserId IncognitoEnabled ContactId | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | DeleteContact ContactName ChatDeleteMode @@ -774,6 +778,8 @@ data ChatResponse | CRContactCode {user :: User, contact :: Contact, connectionCode :: Text} | CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text} | CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text} + | CRContactDomainVerified {user :: User, contact :: Contact, verificationFailure :: Maybe Text} + | CRGroupDomainVerified {user :: User, groupInfo :: GroupInfo, verificationFailure :: Maybe Text} | CRTagsUpdated {user :: User, userTags :: [ChatTag], chatTags :: [ChatTagId]} | CRNewChatItems {user :: User, chatItems :: [AChatItem]} | CRChatItemUpdated {user :: User, chatItem :: AChatItem} @@ -1090,7 +1096,7 @@ data InvitationLinkPlan deriving (Show) data ContactAddressPlan - = CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification} + = CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification, verifiedDomain :: Maybe SimplexDomain} | CAPOwnLink | CAPConnectingConfirmReconnect | CAPConnectingProhibit {contact :: Contact} @@ -1099,7 +1105,7 @@ data ContactAddressPlan deriving (Show) data GroupLinkPlan - = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification} + = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification, verifiedDomain :: Maybe SimplexDomain} | GLPOwnLink {groupInfo :: GroupInfo} | GLPConnectingConfirmReconnect | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} @@ -1399,6 +1405,12 @@ data ChatError | ChatErrorRemoteHost {rhKey :: RHKey, remoteHostError :: RemoteHostError} deriving (Show, Exception) +-- why a resolved SimpleX name could not be used (the name itself resolved; an unregistered name is the agent's NAME NOT_FOUND) +data SimplexDomainError + = SDENoValidLink -- the name's record has no usable contact/channel link + | SDEUnknownDomain -- the resolved link's profile has no name, or a different name + deriving (Eq, Show) + data ChatErrorType = CENoActiveUser | CENoConnectionUser {agentConnId :: AgentConnId} @@ -1420,6 +1432,7 @@ data ChatErrorType | CEChatNotStopped | CEChatStoreChanged | CEInvalidConnReq + | CESimplexDomainNotReady {simplexDomain :: SimplexDomain, simplexDomainError :: SimplexDomainError} | CEUnsupportedConnReq | CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String} | CEConnReqMessageProhibited @@ -1751,6 +1764,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SDE") ''SimplexDomainError) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHE") ''RemoteHostError) diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index ac05550939..cc3e49beb6 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -140,7 +140,7 @@ createActiveUser cc CoreChatOpts {chatRelay} = \case displayName <- T.pack <$> withPrompt "display name" getLine createUser loop False $ mkProfile displayName where - mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing} + mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} createUser onError clientService p = execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, userChatRelay = BoolDef chatRelay, clientService = BoolDef clientService}) 0 `runReaderT` cc >>= \case Right (CRActiveUser user) -> pure user diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index eada7e5a1b..836f4e72fd 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -13,6 +13,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Library.Commands where @@ -56,6 +57,7 @@ import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Simplex.Chat.Library.Subscriber import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), maxXFTPFileSize, mkBadgeStatus, verifyCredential) +import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..)) @@ -91,7 +93,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Util (liftIOEither, zipWith3') import qualified Simplex.Chat.Util as U import Simplex.Chat.Web (webPreviewWorker) -import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSize, maxFileSizeHard) +import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSizeHard) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Agent.Protocol @@ -105,11 +107,11 @@ import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.ShortLink as SL import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF -import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQSupportOff, pattern PQSupportOn) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth) @@ -129,7 +131,7 @@ import qualified UnliftIO.Exception as E import UnliftIO.IO (hClose) import UnliftIO.STM #if defined(dbPostgres) -import Data.Bifunctor (bimap, second) +import Data.Bifunctor (bimap, first, second) import Simplex.Messaging.Agent.Client (SubInfo (..), getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, temporaryOrHostError) #else import Data.Bifunctor (bimap, first, second) @@ -381,14 +383,14 @@ processChatCommand cxt nm = \case user <- withFastStore $ \db -> do user <- createUserRecordAt db (AgentUserId auId) (isTrue userChatRelay) service p True ts mapM_ (setUserServers db user ts) uss - createPresetContactCards db cxt user `catchAllErrors` \_ -> pure () + createPresetContactCards db user `catchAllErrors` \_ -> pure () createNoteFolder db user pure user atomically . writeTVar u $ Just user pure $ CRActiveUser user where - createPresetContactCards :: DB.Connection -> StoreCxt -> User -> ExceptT StoreError IO () - createPresetContactCards db cxt user = do + createPresetContactCards :: DB.Connection -> User -> ExceptT StoreError IO () + createPresetContactCards db user = do createContact db cxt user simplexStatusContactProfile createContact db cxt user simplexTeamContactProfile chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))) @@ -1491,6 +1493,24 @@ processChatCommand cxt nm = \case withCurrentCall contactId $ \user ct call -> updateCallItemStatus user ct call receivedStatus Nothing $> Just call APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile) + APISetUserDomain userId domain_ -> withUserId userId $ \user@User {profile = p@LocalProfile {contactLink, contactDomain}} -> + if (claimDomain <$> contactDomain) == domain_ + then pure $ CRUserProfileNoChange user + else do + cl' <- case domain_ of + Nothing -> pure contactLink + Just domain -> do + UserContactLink {shortLinkDataSet, connLinkContact = CCLink _ sl_} <- withFastStore (`getUserAddress` user) + case sl_ of + Just sl | shortLinkDataSet -> do + NameRecord {nrSimplexContact} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) domain + unless (nameResolvesTo sl nrSimplexContact) $ throwChatError $ CESimplexDomainNotReady domain SDENoValidLink + pure $ Just (CLShort sl) + _ -> throwCmdError "create the address short link and add it to name" + let p' = (fromLocalProfile p :: Profile) {contactDomain = mkDomainClaim <$> domain_, contactLink = cl'} + updateProfile_ user p' True $ withFastStore $ \db -> do + user' <- updateUserProfile db user p' + liftIO $ setUserSimplexDomain db user' domain_ APISetContactPrefs contactId prefs' -> withUser $ \user -> do ct <- withFastStore $ \db -> getContact db cxt user contactId updateContactPrefs user ct prefs' @@ -1829,7 +1849,7 @@ processChatCommand cxt nm = \case APIGroupInfo gId -> withUser $ \user -> CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db cxt user gId) APIGetUpdatedGroupLinkData groupId -> withUser $ \user -> do - gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId + gInfo@GroupInfo {groupProfile = p} <- withFastStore $ \db -> getGroupInfo db cxt user groupId case p of GroupProfile {publicGroup = Just PublicGroupProfile {groupLink = sLnk}} | useRelays' gInfo -> do (_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- getShortLinkConnReq' nm user sLnk @@ -1983,8 +2003,9 @@ processChatCommand cxt nm = \case -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode + -- TODO [badges] bind link and badge to handshake context linkProfile <- presentUserBadge user incognitoProfile $ userProfileDirect user incognitoProfile Nothing True - let userData = contactShortLinkData linkProfile Nothing + let userData = contactShortLinkData linkProfile {contactDomain = Nothing} Nothing userLinkData = UserInvLinkData userData (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userLinkData) Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink @@ -2037,10 +2058,10 @@ processChatCommand cxt nm = \case createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn deleteAgentConnectionAsync (aConnId' conn) pure conn' - APIConnectPlan userId (Just cLink) resolveKnown linkOwnerSig_ -> withUserId userId $ \user -> - uncurry (CRConnectionPlan user) <$> connectPlan user cLink resolveKnown linkOwnerSig_ + APIConnectPlan userId (Just ct) resolveKnown linkOwnerSig_ -> withUserId userId $ \user -> + uncurry (CRConnectionPlan user) <$> connectPlan user ct resolveKnown linkOwnerSig_ APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq - APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do + APIPrepareContact userId accLink verifiedDomain contactSLinkData -> withUserId userId $ \user -> do let ContactShortLinkData {profile, message, business} = contactSLinkData welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId case accLink of @@ -2050,7 +2071,7 @@ processChatCommand cxt nm = \case groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences groupProfile = businessGroupProfile profile groupPreferences gVar <- asks random - (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing + (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing Nothing hostMember <- maybe (throwCmdError "no host member") pure hostMember_ void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let cd = CDGroupRcv gInfo Nothing hostMember @@ -2063,7 +2084,7 @@ processChatCommand cxt nm = \case _ -> Chat cInfo [] emptyChatStats pure $ CRNewPreparedChat user $ AChat SCTGroup chat ACCL _ (CCLink cReq _) -> do - ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId + ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId (True <$ verifiedDomain) void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart) let cd = CDDirectRcv ct createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing Nothing @@ -2075,14 +2096,10 @@ processChatCommand cxt nm = \case Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} _ -> Chat cInfo [] emptyChatStats pure $ CRNewPreparedChat user $ AChat SCTDirect chat - APIPrepareGroup userId ccLink direct groupSLinkData -> withUserId userId $ \user -> do - let GroupShortLinkData {groupProfile = gp@GroupProfile {description}, publicGroupData = publicGroupData_} = groupSLinkData - publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ + APIPrepareGroup userId ccLink direct verifiedDomain groupSLinkData -> withUserId userId $ \user -> do + let GroupShortLinkData {groupProfile = GroupProfile {description}} = groupSLinkData welcomeSharedMsgId <- forM description $ \_ -> getSharedMsgId - let useRelays = not direct - subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember - gVar <- asks random - (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ + (gInfo, hostMember_) <- preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId (True <$ verifiedDomain) void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let cd = maybe (CDChannelRcv gInfo Nothing) (CDGroupRcv gInfo Nothing) hostMember_ cInfo = GroupChat gInfo Nothing @@ -2265,10 +2282,28 @@ processChatCommand cxt nm = \case CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq - Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do - (ccLink, plan) <- connectPlan user cLink False Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e + Connect incognito (Just ct) -> withUser $ \user -> do + let con m cReq = pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)) + (ccLink, plan) <- connectPlan user ct False Nothing `catchAllErrors` \e -> case ct of + ACTarget m (CTFullContact cReq) -> con m cReq + ACTarget m (CTInv (CLFull cReq)) -> con m cReq + _ -> throwError e connectWithPlan user incognito ccLink plan Connect _ Nothing -> throwChatError CEInvalidConnReq + APIVerifyContactDomain contactId -> withUser $ \user -> do + ct@Contact {profile = LocalProfile {contactDomain}, preparedContact} <- withFastStore $ \db -> getContact db cxt user contactId + let connLink_ = preparedContact >>= \PreparedContact {connLinkToConnect = ACCL m (CCLink _ sLnk_)} -> ACSL m <$> sLnk_ + domain <- maybe (throwCmdError "contact has no name to verify") pure contactDomain + (verified, reason) <- verifyEntityDomain user nm NTContact domain connLink_ + ct' <- maybe (pure ct) (\v -> withFastStore' $ \db -> setContactDomainVerified db user ct v) verified + pure $ CRContactDomainVerified user ct' reason + APIVerifyGroupDomain groupId -> withUser $ \user -> do + g@GroupInfo {groupProfile = GroupProfile {publicGroup}, preparedGroup} <- withFastStore $ \db -> getGroupInfo db cxt user groupId + let connLink_ = preparedGroup >>= \PreparedGroup {connLinkToConnect = CCLink _ sLnk_} -> ACSL SCMContact <$> sLnk_ + domain <- maybe (throwCmdError "group has no name to verify") pure $ publicGroup >>= publicGroupAccess >>= groupDomainClaim + (verified, reason) <- verifyEntityDomain user nm NTPublicGroup domain connLink_ + g' <- maybe (pure g) (\v -> withFastStore' $ \db -> setGroupDomainVerified db user g v) verified + pure $ CRGroupDomainVerified user g' reason APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db cxt user contactId ccLink <- case contactLink of @@ -2284,7 +2319,7 @@ processChatCommand cxt nm = \case toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e ConnectSimplex incognito -> withUser $ \user -> do - plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing)) + plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing Nothing)) connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing @@ -2298,16 +2333,20 @@ processChatCommand cxt nm = \case Left e -> throwError $ ChatErrorStore e Right _ -> throwError $ ChatErrorStore SEDuplicateContactLink subMode <- chatReadVar subscriptionMode + gVar <- asks random + rootKey@(rootPubKey, rootPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar + let entityId = C.sha256Hash $ C.pubKeyBytes rootPubKey + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True Nothing + ccLink' <- shortenCreatedLink ccLink -- TODO [relays] relay: add identity, key to link data? userData <- if isTrue userChatRelay then pure $ relayShortLinkData (userProfileDirect user Nothing Nothing True) else (`contactShortLinkData` Nothing) <$> presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True) let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} - (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) Nothing IKPQOn subMode - ccLink' <- shortenCreatedLink ccLink + connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData IKPQOn subMode let ccLink'' = if isTrue userChatRelay then setShortLinkType CCTRelay ccLink' else ccLink' - withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode + withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode rootPrivKey pure $ CRUserContactLinkCreated user ccLink'' CreateMyAddress -> withUser $ \User {userId} -> processChatCommand cxt nm $ APICreateMyAddress userId @@ -2538,7 +2577,7 @@ processChatCommand cxt nm = \case then throwError e else do let relayResults = map toRelayResult results - toRelayResult (r, Left e) = AddRelayResult r (Just e) + toRelayResult (r, Left e') = AddRelayResult r (Just e') toRelayResult (r, Right _) = AddRelayResult r Nothing pure $ CRPublicGroupCreationFailed user relayResults where @@ -3094,11 +3133,15 @@ processChatCommand cxt nm = \case updateGroupProfileByName gName $ \p -> p {description} ShowGroupDescription gName -> withUser $ \user -> CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db cxt user gName) - SetPublicGroupAccess gName access -> withUser $ \user -> do - gInfo@GroupInfo {groupProfile = p@GroupProfile {publicGroup}} <- withStore $ \db -> - getGroupIdByName db user gName >>= getGroupInfo db cxt user + APISetPublicGroupAccess gId access@PublicGroupAccess {groupDomainClaim = newClaim} -> withUser $ \user -> do + gInfo@GroupInfo {groupProfile = p@GroupProfile {publicGroup}} <- withStore $ \db -> getGroupInfo db cxt user gId case publicGroup of - Just pg -> runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}} + Just pg@PublicGroupProfile {groupLink, publicGroupAccess = existingAccess} -> do + forM_ (claimDomain <$> newClaim) $ \newDomain -> + when (Just newDomain /= (claimDomain <$> (existingAccess >>= groupDomainClaim))) $ do + NameRecord {nrSimplexChannel} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) newDomain + unless (nameResolvesTo groupLink nrSimplexChannel) $ throwChatError $ CESimplexDomainNotReady newDomain SDENoValidLink + runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}} Nothing -> throwChatError $ CECommandError "not a public group" APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db cxt user groupId @@ -3213,6 +3256,9 @@ processChatCommand cxt nm = \case sqSecured <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode let newStatus = if sqSecured then ConnSndReady else ConnJoined void $ withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus + SetPublicGroupAccess gName access -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand cxt nm $ APISetPublicGroupAccess groupId access CreateGroupLink gName mRole -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand cxt nm $ APICreateGroupLink groupId mRole @@ -3741,9 +3787,7 @@ processChatCommand cxt nm = \case -- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile profileToSend <- presentUserBadge user incognitoProfile $ case gInfo_ of - Just gInfo_' -> - let allowSimplexLinks = maybe True groupUserAllowSimplexLinks gInfo_' - in userProfileInGroup' user allowSimplexLinks incognitoProfile + Just gInfo_' -> userProfileInGroup' user gInfo_' incognitoProfile Nothing -> userProfileDirect user incognitoProfile Nothing True dm <- case gInfo_ of Just (Just gInfo) | useRelays' gInfo -> case relayMemberId_ of @@ -3819,16 +3863,16 @@ processChatCommand cxt nm = \case -- non-incognito (filtered above), so the user's badge is presented; a profile update keeps the badge instead of clearing it ctSndEvent :: ChangedProfileContact -> CM (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json) ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = do - p <- presentUserBadge user' Nothing mergedProfile' - pure (ConnectionId connId, Nothing, XInfo p) + p'' <- presentUserBadge user' Nothing mergedProfile' + pure (ConnectionId connId, Nothing, XInfo p'') ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq ctMsgReq ChangedProfileContact {conn} = fmap $ \SndMessage {msgId, msgBody} -> (conn, MsgFlags {notification = hasNotification XInfo_}, (vrValue msgBody, [msgId])) setMyAddressData :: User -> UserContactLink -> CM UserContactLink - setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _sLnk_, addressSettings} = do + setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _, addressSettings} = do conn <- withFastStore $ \db -> getUserAddressConnection db cxt user - shortLinkProfile <- presentUserBadge user Nothing $ userProfileDirect user Nothing Nothing True + shortLinkProfile <- presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True) -- TODO [short links] do not save address to server if data did not change, spinners, error handling let userData | isTrue userChatRelay = relayShortLinkData shortLinkProfile @@ -4051,9 +4095,8 @@ processChatCommand cxt nm = \case conn <- createRelayConnection db cxt user (groupMemberId' relayMember) connId ConnPrepared chatV subMode pure (relayMember, conn, groupRelay) let GroupMember {memberRole = userRole, memberId = userMemberId} = membership - allowSimplexLinks = groupUserAllowSimplexLinks gInfo GroupMember {memberId = relayMemberId} = relayMember - membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership + membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership let relayInv = GroupRelayInvitation { fromMember = MemberIdRole userMemberId userRole, fromMemberProfile = membershipProfile, @@ -4129,8 +4172,8 @@ processChatCommand cxt nm = \case pure (gId, chatSettings) _ -> throwCmdError "not supported" processChatCommand cxt nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings - connectPlan :: User -> AConnectionLink -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) - connectPlan user (ACL SCMInvitation cLink) _ sig_ = case cLink of + connectPlan :: User -> AConnectTarget -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) + connectPlan user (ACTarget SCMInvitation (CTInv cLink)) _ sig_ = case cLink of CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing Nothing CLShort l -> do let l' = serverShortLink l @@ -4151,51 +4194,80 @@ processChatCommand cxt nm = \case invitationReqAndPlan cReq sLnk_ cld ov = do plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError) pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) - connectPlan user (ACL SCMContact cLink) resolveKnown sig_ = case cLink of - CLFull cReq -> do + connectPlan user (ACTarget SCMContact ct) resolveKnown sig_ = case ct of + CTFullContact cReq -> do plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError) pure (ACCL SCMContact $ CCLink cReq Nothing, plan) - CLShort l@(CSLContact _ ct _ _) -> - case ct of + CTShortContact nl -> + case ctType of CCTContact -> knownLinkPlans >>= \case Just r -> pure r Nothing -> do + l' <- resolveSLink (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' + contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) + let linkProfile_ = (\ContactShortLinkData {profile} -> profile) <$> contactSLinkData_ + linkDomain_ = linkProfile_ >>= \Profile {contactDomain} -> claimDomain <$> contactDomain + verifiedDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing + refreshContact ct' = case (verifiedDomain, linkProfile_) of + (Just _, Just p) -> updateContactFromLinkData user ct' p + _ -> pure ct' + forM_ verifiedDomain $ \nameDomain -> + unless (linkDomain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain withFastStore' (\db -> getContactWithoutConnViaShortAddress db cxt user l') >>= \case - Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct')) + Just ct' | not (contactDeleted ct') -> do + ct'' <- refreshContact ct' + pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct'')) _ -> do - contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) let ContactLinkData _ UserContactData {owners} = cData ov = verifyLinkOwner rootKey owners l' sig_ plan <- contactRequestPlan user cReq contactSLinkData_ ov - pure (con cReq, plan) + case plan of + CPContactAddress cap@(CAPOk {}) -> pure (con l' cReq, CPContactAddress cap {verifiedDomain}) + CPContactAddress (CAPKnown ct') -> do + ct'' <- refreshContact ct' + pure (con l' cReq, CPContactAddress (CAPKnown ct'')) + CPContactAddress (CAPContactViaAddress ct') -> do + ct'' <- refreshContact ct' + pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct'')) + _ -> pure (con l' cReq, plan) where knownLinkPlans = withFastStore $ \db -> - liftIO (getUserContactLinkViaShortLink db user l') >>= \case - Just UserContactLink {connLinkContact = CCLink cReq _} -> pure $ Just (con cReq, CPContactAddress CAPOwnLink) + liftIO (getUserContactLinkViaTarget db user nl') >>= \case + Just UserContactLink {connLinkContact} -> pure $ Just (ACCL SCMContact connLinkContact, CPContactAddress CAPOwnLink) Nothing -> - getContactViaShortLinkToConnect db cxt user l' >>= \case - Just (cReq, ct') -> pure $ if contactDeleted ct' then Nothing else Just (con cReq, CPContactAddress (CAPKnown ct')) - Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db cxt user l' + getContactToConnect db cxt user nl' >>= \case + Just (ccl, ct') -> pure $ if contactDeleted ct' then Nothing else Just (ACCL SCMContact ccl, CPContactAddress (CAPKnown ct')) + Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl' CCTGroup -> groupShortLinkPlan CCTChannel -> groupShortLinkPlan CCTRelay -> throwCmdError "chat relay links are not supported in this version" where - l' = serverShortLink l - con cReq = ACCL SCMContact $ CCLink cReq (Just l') - gPlan (cReq, g) = if memberRemoved (membership g) then Nothing else Just (con cReq, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef []))) + nl' = case nl of + CTLink sl -> CTLink (serverShortLink sl) + CTName _ -> nl + ctType = case nl of + CTLink (CSLContact _ t _ _) -> t + CTName SimplexNameInfo {nameType = NTContact} -> CCTContact + CTName SimplexNameInfo {nameType = NTPublicGroup} -> CCTChannel + resolveSLink = case nl' of + CTLink l' -> pure l' + CTName n -> serverShortLink <$> resolveNameLink user n + con l' cReq = ACCL SCMContact $ CCLink cReq (Just l') + gPlan (ccl, g) = if memberRemoved (membership g) then Nothing else Just (ACCL SCMContact ccl, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef []))) groupShortLinkPlan = knownLinkPlans >>= \case Just (_, CPGroupLink (GLPKnown g _ _ _)) | resolveKnown -> resolveKnownGroup g Just r -> pure r Nothing -> do + l' <- resolveSLink (fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData if - | not direct && unsupportedGroupType groupSLinkData_ -> pure (con (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_)) - | not direct && null relays -> pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) + | not direct && unsupportedGroupType groupSLinkData_ -> pure (con l' (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_)) + | not direct && null relays -> pure (con l' (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) | otherwise -> do let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} @@ -4206,17 +4278,29 @@ processChatCommand cxt nm = \case (Nothing, Nothing) -> pure () _ -> throwChatError CEInvalidConnReq let ov = verifyLinkOwner rootKey owners l' sig_ + verifiedDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov - pure (con cReq, plan) + forM_ verifiedDomain $ \nameDomain -> + let domain_ = (\GroupProfile {publicGroup} -> claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) =<< case plan of + CPGroupLink (GLPOk _ (Just GroupShortLinkData {groupProfile}) _ _) -> Just groupProfile + CPGroupLink (GLPKnown GroupInfo {groupProfile} _ _ _) -> Just groupProfile + CPGroupLink (GLPOwnLink GroupInfo {groupProfile}) -> Just groupProfile + CPGroupLink (GLPConnectingProhibit (Just GroupInfo {groupProfile})) -> Just groupProfile + _ -> (\GroupShortLinkData {groupProfile} -> groupProfile) <$> groupSLinkData_ + in unless (domain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain + pure $ case plan of + CPGroupLink glp@(GLPOk {}) -> (con l' cReq, CPGroupLink glp {verifiedDomain}) + _ -> (con l' cReq, plan) where unsupportedGroupType = \case Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel _ -> False knownLinkPlans = withFastStore $ \db -> - liftIO (getGroupInfoViaUserShortLink db cxt user l') >>= \case - Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g)) - Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db cxt user l' + liftIO (getGroupInfoViaUserTarget db cxt user nl') >>= \case + Just (ccl, g) -> pure $ Just (ACCL SCMContact ccl, CPGroupLink (GLPOwnLink g)) + Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl' resolveKnownGroup g = do + l' <- resolveSLink (fd@FixedLinkData {rootKey = rk}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData let ov = verifyLinkOwner rk owners l' sig_ @@ -4224,7 +4308,16 @@ processChatCommand cxt nm = \case (g', updated) <- case groupSLinkData_ of Just sLinkData -> updateGroupFromLinkData user g sLinkData _ -> pure (g, False) - pure (con (linkConnReq fd), CPGroupLink (GLPKnown g' (BoolDef updated) ov (ListDef glOwners))) + pure (con l' (linkConnReq fd), CPGroupLink (GLPKnown g' (BoolDef updated) ov (ListDef glOwners))) + -- resolve a name to its first contact/channel short link + resolveNameLink :: User -> SimplexNameInfo -> CM (ConnShortLink 'CMContact) + resolveNameLink user SimplexNameInfo {nameType, nameDomain} = do + NameRecord {nrSimplexContact, nrSimplexChannel} <- + withAgent $ \a -> resolveSimplexName a nm (aUserId user) nameDomain + let (candidates, ctType) = case nameType of + NTContact -> (nrSimplexContact, CCTContact) + NTPublicGroup -> (nrSimplexChannel, CCTChannel) + maybe (throwChatError $ CESimplexDomainNotReady nameDomain SDENoValidLink) pure $ firstNameLink ctType candidates connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse connectWithPlan user@User {userId} incognito ccLink plan | connectionPlanProceed plan = do @@ -4232,13 +4325,14 @@ processChatCommand cxt nm = \case case plan of CPContactAddress (CAPContactViaAddress Contact {contactId}) -> processChatCommand cxt nm $ APIConnectContactViaAddress userId incognito contactId - CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _) - | ACCL SCMContact ccl <- ccLink -> joinChannelViaRelays ccl gld + CPContactAddress (CAPOk (Just sld) _ vName@(Just _)) -> connectContactViaName sld vName + CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _ vName) + | ACCL SCMContact ccl <- ccLink -> joinChannelViaRelays ccl gld vName _ -> processChatCommand cxt nm $ APIConnect userId incognito $ Just ccLink | otherwise = pure $ CRConnectionPlan user ccLink plan where - joinChannelViaRelays :: CreatedLinkContact -> GroupShortLinkData -> CM ChatResponse - joinChannelViaRelays ccl gld = do + joinChannelViaRelays :: CreatedLinkContact -> GroupShortLinkData -> Maybe SimplexDomain -> CM ChatResponse + joinChannelViaRelays ccl gld vName = do GroupInfo {groupId} <- prepareChannelGroup processChatCommand cxt nm APIConnectPreparedGroup {groupId, incognito, ownerContact = Nothing, msgContent_ = Nothing} `catchAllErrors` \e -> do @@ -4246,13 +4340,19 @@ processChatCommand cxt nm = \case throwError e where prepareChannelGroup = - processChatCommand cxt nm (APIPrepareGroup userId ccl False gld) >>= \case + processChatCommand cxt nm (APIPrepareGroup userId ccl False vName gld) >>= \case CRNewPreparedChat _ (AChat SCTGroup (Chat (GroupChat gInfo _) _ _)) -> pure gInfo _ -> throwChatError $ CEException "joinChannelViaRelays: unexpected response from APIPrepareGroup" deletePreparedChannel groupId = do gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId deleteGroupConnections user gInfo False withFastStore' $ \db -> deleteGroup db user gInfo + connectContactViaName :: ContactShortLinkData -> Maybe SimplexDomain -> CM ChatResponse + connectContactViaName sld vName = + processChatCommand cxt nm (APIPrepareContact userId ccLink vName sld) >>= \case + CRNewPreparedChat _ (AChat SCTDirect (Chat (DirectChat Contact {contactId}) _ _)) -> + processChatCommand cxt nm (APIConnectPreparedContact contactId incognito Nothing) + _ -> throwChatError $ CEException "connectContactViaName: unexpected response from APIPrepareContact" invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan invitationRequestPlan user cReq cld ov = do maybe (CPInvitationLink (ILPOk cld ov)) (invitationEntityPlan cld ov) @@ -4292,13 +4392,13 @@ processChatCommand cxt nm = \case Nothing -> withFastStore' (\db -> getContactWithoutConnViaAddress db cxt user cReqSchemas) >>= \case Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct) - _ -> pure $ CPContactAddress (CAPOk cld ov) + _ -> pure $ CPContactAddress (CAPOk cld ov Nothing) Just (RcvDirectMsgConnection Connection {connStatus} Nothing) - | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk cld ov) + | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk cld ov Nothing) | otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect Just (RcvDirectMsgConnection _ (Just ct)) | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) - | contactDeleted ct -> pure $ CPContactAddress (CAPOk cld ov) + | contactDeleted ct -> pure $ CPContactAddress (CAPOk cld ov Nothing) | otherwise -> pure $ CPContactAddress (CAPKnown ct) -- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?) Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing @@ -4313,12 +4413,12 @@ processChatCommand cxt nm = \case connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db cxt user cReqHashes gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db cxt user cReqHashes case (gInfo_, connEnt_) of - (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov) + (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing) -- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?) (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | otherwise -> pure $ CPGroupLink (GLPOk linkInfo gld ov) + | otherwise -> pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing) (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" (Just gInfo, _) -> groupPlan gInfo linkInfo gld ov groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan @@ -4327,7 +4427,7 @@ processChatCommand cxt nm = \case | not (memberActive membership) && not (memberRemoved membership) = pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo (BoolDef False) ov (ListDef [])) - | otherwise = pure $ CPGroupLink (GLPOk linkInfo gld ov) + | otherwise = pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing) contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) contactCReqSchemas crData = ( CRContactUri crData {crScheme = SSSimplex}, @@ -4665,11 +4765,56 @@ processChatCommand cxt nm = \case gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId a $ SRGroup gId scope (sendAsGroup' gInfo scope) _ -> throwCmdError "not supported" + preparedGroupFromLink :: User -> CreatedLinkContact -> DirectLink -> GroupShortLinkData -> Maybe SharedMsgId -> Maybe Bool -> CM (GroupInfo, Maybe GroupMember) + preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId nameVerified = do + let GroupShortLinkData {groupProfile = gp, publicGroupData = publicGroupData_} = groupSLinkData + publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ + useRelays = not direct + subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember + gVar <- asks random + withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ nameVerified + getSharedMsgId :: CM SharedMsgId getSharedMsgId = do gVar <- asks random liftIO $ SharedMsgId <$> encodedRandomBytes gVar 12 +firstNameLink :: ContactConnType -> [Text] -> Maybe (ConnShortLink 'CMContact) +firstNameLink ctType = foldr (\t r -> nameLink t <|> r) Nothing + where + nameLink t = case strDecode @(ConnShortLink 'CMContact) (encodeUtf8 t) of + Right sl@(CSLContact _ ct _ _) | ct == ctType -> Just sl + _ -> Nothing + +nameResolvesTo :: ConnShortLink 'CMContact -> [Text] -> Bool +nameResolvesTo sLnk = any (either (const False) (sameShortLinkContact sLnk) . strDecode . encodeUtf8) + +verifyEntityDomain :: User -> NetworkRequestMode -> SimplexNameType -> SimplexDomainClaim -> Maybe AConnShortLink -> CM (Maybe Bool, Maybe Text) +verifyEntityDomain user nm nameType SimplexDomainClaim {domain = StrJSON domain, proof = proof_} connLink_ = case (proof_, connLink_) of + (Nothing, _) -> pure (Nothing, Just "no name proof to verify") + (_, Nothing) -> pure (Nothing, Just "no connection link to check the name against") + (Just proof, Just (ACSL SCMContact profileSLnk)) -> do + NameRecord {nrSimplexContact, nrSimplexChannel} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) domain + let resolvedLinks = case nameType of + NTContact -> nrSimplexContact + NTPublicGroup -> nrSimplexChannel + if not (nameResolvesTo profileSLnk resolvedLinks) + then pure (Just False, Just "the name does not resolve to this address") + else do + ok <- verifyDomainProof proof profileSLnk + pure (Just ok, if ok then Nothing else Just "the name proof was not signed by this address's owner") + (Just _, Just _) -> pure (Nothing, Just "unexpected connection link type for name verification") + where + verifyDomainProof :: SimplexDomainProof -> ShortLinkContact -> CM Bool + verifyDomainProof SimplexDomainProof {linkOwnerId, presHeader, signature} sLnk@(CSLContact _ ct srv key) = do + (FixedLinkData {rootKey}, ContactLinkData _ UserContactData {owners}) <- getShortLinkConnReq nm user sLnk + let ownerKey_ = case linkOwnerId of + Nothing -> Just rootKey + Just (StrJSON oid) -> ownerKey <$> find (\OwnerAuth {ownerId} -> ownerId == oid) owners + pure $ maybe False (\k -> C.verify' k signature proofPayload) ownerKey_ + where + proofPayload = strEncode (Str "simplex_domain_v1", presHeader, domain, ct, srv, key) + data ConnectViaContactResult = CVRConnectedContact Contact | CVRSentInvitation Connection (Maybe Profile) @@ -5146,6 +5291,7 @@ chatCommandP = "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP), "/_call get" $> APIGetCallInvitations, "/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP), + "/_set domain " *> (APISetUserDomain <$> A.decimal <*> optional (A.space *> strP)), "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias #" *> (APISetGroupAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), @@ -5286,6 +5432,7 @@ chatCommandP = "/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <* A.space <*> (Just <$> msgTextP)), "/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing), "/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP), + "/_public group access #" *> (APISetPublicGroupAccess <$> A.decimal <* A.space <*> jsonP), "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)), "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), @@ -5303,8 +5450,8 @@ chatCommandP = "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> ((" resolve=" *> onOffP) <|> pure False) <*> optional (" sig=" *> jsonP)), - "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), - "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <* A.space <*> jsonP), + "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <*> optional (" domain=" *> strP) <* A.space <*> jsonP), + "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <*> optional (" domain=" *> strP) <* A.space <*> jsonP), "/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal), "/_set group user #" *> (APIChangePreparedGroupUser <$> A.decimal <* A.space <*> A.decimal), "/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)), @@ -5315,6 +5462,8 @@ chatCommandP = "/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal), ("/connect" <|> "/c") *> (AddContact <$> incognitoP), ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), + "/_verify domain @" *> (APIVerifyContactDomain <$> A.decimal), + "/_verify domain #" *> (APIVerifyGroupDomain <$> A.decimal), ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, @@ -5490,10 +5639,10 @@ chatCommandP = onOffP = ("on" $> True) <|> ("off" $> False) publicGroupAccessP = do groupWebPage <- optional (" web=" *> (safeDecodeUtf8 <$> A.takeTill A.isSpace)) - groupDomain <- optional (" domain=" *> (safeDecodeUtf8 <$> A.takeTill A.isSpace)) + groupDomain <- optional (" domain=" *> strP) domainWebPage <- (" domain_page=" *> onOffP) <|> pure False allowEmbedding <- (" embed=" *> onOffP) <|> pure False - pure PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding} + pure PublicGroupAccess {groupWebPage, groupDomainClaim = mkDomainClaim <$> groupDomain, domainWebPage, allowEmbedding} profileNameDescr = (,) <$> displayNameP <*> shortDescrP -- 'Help with bot':'link ','Menu of commands':[...] botCommandsP :: Parser [ChatBotCommand] @@ -5514,7 +5663,7 @@ chatCommandP = newUserP relay = do (cName, shortDescr) <- profileNameDescr service <- (" service=" *> onOffP) <|> pure False - let profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing} + let profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef relay, clientService = BoolDef service} newBotUserP = do files_ <- optional $ "files=" *> onOffP <* A.space @@ -5523,7 +5672,7 @@ chatCommandP = let preferences = case files_ of Just True -> Nothing _ -> Just (emptyChatPrefs :: Preferences) {files = Just FilesPreference {allow = FANo}} - profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing} + profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing, contactDomain = Nothing} pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef False, clientService = BoolDef service} jsonP :: J.FromJSON a => Parser a jsonP = J.eitherDecodeStrict' <$?> A.takeByteString @@ -5635,9 +5784,9 @@ chatCommandP = srvRolesP = srvRoles <$?> A.takeTill (\c -> c == ':' || c == ',') where srvRoles = \case - "off" -> Right $ ServerRoles False False - "proxy" -> Right ServerRoles {storage = False, proxy = True} - "storage" -> Right ServerRoles {storage = True, proxy = False} + "off" -> Right $ ServerRoles False False False + "proxy" -> Right ServerRoles {storage = False, proxy = True, names = False} + "storage" -> Right ServerRoles {storage = True, proxy = False, names = False} "on" -> Right allRoles _ -> Left "bad ServerRoles" netCfgP = do diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index c7f44d125d..d34e08401b 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -53,7 +53,8 @@ import Data.Text.Encoding (encodeUtf8) import Data.Time (addUTCTime) import Data.Time.Calendar (fromGregorian) import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds, secondsToDiffTime) -import Simplex.Chat.Badges (BadgeCredential (..), BadgePresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Badges (BadgeCredential (..), ProofPresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Names (SimplexDomainClaim (..), claimDomain) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Files @@ -1223,13 +1224,14 @@ introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn sendGroupMemberMessages user gInfo conn evts userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile -userProfileInGroup user = userProfileInGroup' user . groupUserAllowSimplexLinks +userProfileInGroup user g = userProfileInGroup' user (Just g) {-# INLINE userProfileInGroup #-} -userProfileInGroup' :: User -> Bool -> Maybe Profile -> Profile -userProfileInGroup' User {profile = p} allowSimplexLinks incognitoProfile = +-- Nothing group ⇒ no redaction (e.g. joining via a link with no group profile yet). +userProfileInGroup' :: User -> Maybe GroupInfo -> Maybe Profile -> Profile +userProfileInGroup' User {profile = p} mg incognitoProfile = let p' = fromMaybe (fromLocalProfile p) incognitoProfile - in redactedMemberProfile allowSimplexLinks p' + in maybe p' (\g -> redactedMemberProfile g (membership g) p') mg memberInfo :: GroupInfo -> GroupMember -> MemberInfo memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, activeConn} = @@ -1237,16 +1239,18 @@ memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, a { memberId, memberRole, v = ChatVersionRange . peerChatVRange <$> activeConn, - profile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile memberProfile, + profile = redactedMemberProfile g m $ fromLocalProfile memberProfile, memberKey = MemberKey <$> memberPubKey } - where - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && groupFeatureMemberAllowed SGFDirectMessages m g -redactedMemberProfile :: Bool -> Profile -> Profile -redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDescr, image, peerType, badge} = - Profile {displayName, fullName, shortDescr = removeSimplexLink =<< shortDescr, image, contactLink = Nothing, preferences = Nothing, peerType, badge} +redactedMemberProfile :: GroupInfo -> GroupMember -> Profile -> Profile +redactedMemberProfile g m Profile {displayName, fullName, shortDescr, image, contactLink = lnk, peerType, badge, contactDomain} = + Profile {displayName, fullName, shortDescr = removeSimplexLink =<< shortDescr, image, contactLink, preferences = Nothing, peerType, badge, contactDomain = redactedDomain} where + contactLink = if allowSimplexLinks then lnk else Nothing + redactedDomain = if allowDirect then (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain else Nothing + allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g + allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect removeSimplexLink s | allowSimplexLinks = Just s | hasObfuscatedSimplexLink s = Nothing @@ -1463,6 +1467,19 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = G Just PublicGroupData {publicMemberCount} -> Just publicMemberCount /= localCount _ -> False +updateContactFromLinkData :: User -> Contact -> Profile -> CM Contact +updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {contactDomain = prevClaim, contactDomainVerified}} linkProfile@Profile {contactDomain = newClaim} + | profileChanged || verifyChanged = do + cxt <- chatStoreCxt + withFastStore $ \db -> do + ct' <- updateContactProfile db cxt user ct linkProfile + if verifyChanged then liftIO $ setContactDomainVerified db user ct' True else pure ct' + | otherwise = pure ct + where + profileChanged = fromLocalProfile profile /= linkProfile + claimChanged = (claimDomain <$> prevClaim) /= (claimDomain <$> newClaim) + verifyChanged = contactDomainVerified /= Just True || claimChanged + -- TODO [relays] owner: set owners on updating link data (multi-owner) groupLinkData :: GroupInfo -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData) groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}, membership = GroupMember {memberId}, groupKeys} GroupLink {groupLinkId} groupRelays = @@ -2050,6 +2067,7 @@ presentUserBadge User {profile = LocalProfile {localBadge}} incognitoProfile p = Left e -> p <$ logError ("presentUserBadge: proof generation failed: " <> T.pack e) _ -> pure p + -- receiving side of contact/invitation link data: verify the badge proof from the link profile -- and set the crypto-free display badge for the UI (the raw proof stays in profile for APIPrepareContact) linkDataBadge :: ContactShortLinkData -> CM ContactShortLinkData @@ -2349,9 +2367,8 @@ sendGroupMessages user gInfo scope asGroup members events = do _ -> False sendProfileUpdate = do let members' = filter (`supportsVersion` memberProfileUpdateVersion) members - allowSimplexLinks = groupUserAllowSimplexLinks gInfo -- shouldSendProfileUpdate excludes incognito membership, so the badge is presented - profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile p + profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile gInfo (membership gInfo) $ fromLocalProfile p void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate currentTs <- liftIO getCurrentTime withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs @@ -3091,7 +3108,8 @@ simplexTeamContactProfile = contactLink = Just $ CLFull adminContactReq, peerType = Nothing, preferences = Nothing, - badge = Nothing + badge = Nothing, + contactDomain = Nothing } simplexStatusContactProfile :: Profile @@ -3104,7 +3122,8 @@ simplexStatusContactProfile = contactLink = Just (either error CLFull $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"), peerType = Just CPTBot, preferences = Nothing, - badge = Nothing + badge = Nothing, + contactDomain = Nothing } timeItToView :: String -> CM' a -> CM' a diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index c964df2b3d..9cc6778122 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -381,8 +381,6 @@ processAgentMessageConn :: StoreCxt -> User -> ACorrId -> ConnId -> AEvent 'AECo processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = do -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, -- as in this case no need to ACK message - we can't process messages for this connection anyway. - -- SEDBException will be re-trown as CRITICAL as it is likely to indicate a temporary database condition - -- that will be resolved with app restart. entity <- critical agentConnId $ withStore (\db -> getConnectionEntity db cxt user $ AgentConnId agentConnId) >>= updateConnStatus case agentMessage of END -> case entity of @@ -838,8 +836,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpMemInfo memId _memProfile | sameMemberId memId m -> do let GroupMember {memberId = membershipMemId} = membership - allowSimplexLinks = groupUserAllowSimplexLinks gInfo - membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership + membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership -- TODO update member profile -- [async agent commands] no continuation needed, but command should be asynchronous for stability allowAgentConnectionAsync user conn' confId $ XGrpMemInfo membershipMemId membershipProfile @@ -1400,13 +1397,13 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = CFSetShortLink -> case (ucGroupId_, auData) of (Just groupId, UserContactLinkData UserContactData {relays = relayLinks}) -> do - (gInfo, gLink, relays, relaysChanged, newlyActiveLinks, newlyActiveGMIds) <- withStore $ \db -> do + (gInfo, gLink, relays, relaysChanged, newlyActiveLinks) <- withStore $ \db -> do gInfo <- getGroupInfo db cxt user groupId gLink <- getGroupLink db user gInfo relays <- liftIO $ getGroupRelays db gInfo - (relays', changed, newlyActiveLinks, newlyActiveGMIds) <- liftIO $ foldrM (updateRelay db) ([], False, [], []) relays + (relays', changed, newlyActiveLinks) <- liftIO $ foldrM (updateRelay db) ([], False, []) relays liftIO $ setGroupInProgressDone db gInfo - pure (gInfo, gLink, relays', changed, newlyActiveLinks, newlyActiveGMIds) + pure (gInfo, gLink, relays', changed, newlyActiveLinks) toView $ CEvtGroupLinkDataUpdated user gInfo gLink relays relaysChanged let GroupSummary {publicMemberCount} = groupSummary gInfo -- Owner is counted in publicMemberCount; > 1 means at least one subscriber. @@ -1424,16 +1421,16 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = unless (null recipients) $ void $ sendGroupMessages user gInfo Nothing False recipients events where - updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact], [GroupMemberId]) -> IO ([GroupRelay], Bool, [ShortLinkContact], [GroupMemberId]) - updateRelay db relay@GroupRelay {groupMemberId, relayLink, relayStatus} (acc, changed, newlyActiveLinks, newlyActiveGMIds) = + updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact]) -> IO ([GroupRelay], Bool, [ShortLinkContact]) + updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed, newlyActiveLinks) = case relayLink of Just rLink -- version is gated upstream at publish (getPublishableGroupRelays): an RSAccepted relay -- whose link is in the published data is necessarily pre-roster, so activate it too | rLink `elem` relayLinks && (relayStatus == RSAcknowledgedRoster || relayStatus == RSAccepted) -> do relay' <- updateRelayStatus db relay RSActive - pure (relay' : acc, True, rLink : newlyActiveLinks, groupMemberId : newlyActiveGMIds) - | rLink `elem` relayLinks -> pure (relay : acc, changed, newlyActiveLinks, newlyActiveGMIds) + pure (relay' : acc, True, rLink : newlyActiveLinks) + | rLink `elem` relayLinks -> pure (relay : acc, changed, newlyActiveLinks) | relayStatus == RSActive -> do -- Relay link absent from link data — deactivate. -- RSAccepted relays are not deactivated: their own link data update @@ -1442,8 +1439,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- TODO the SMP server, but this owner won't receive a LINK callback for it -- TODO (LINK only fires in response to own setConnShortLink calls). relay' <- updateRelayStatus db relay RSInactive - pure (relay' : acc, True, newlyActiveLinks, newlyActiveGMIds) - _ -> pure (relay : acc, changed, newlyActiveLinks, newlyActiveGMIds) + pure (relay' : acc, True, newlyActiveLinks) + _ -> pure (relay : acc, changed, newlyActiveLinks) _ -> throwChatError $ CECommandError "LINK event expected for a group link only" _ -> throwChatError $ CECommandError "unexpected cmdFunction" MERR _ err -> do @@ -2812,8 +2809,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = | otherwise = pure m where - contentChanged = not (sameProfileContent (redactedMemberProfile allowSimplexLinks (fromLocalProfile p)) (redactedMemberProfile allowSimplexLinks p')) - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo && groupFeatureMemberAllowed SGFDirectMessages m gInfo + contentChanged = not (sameProfileContent (redactedMemberProfile gInfo m (fromLocalProfile p)) (redactedMemberProfile gInfo m p')) updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of Just bc | isMainBusinessMember bc m -> do g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' @@ -3228,8 +3224,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure toMember subMode <- chatReadVar subscriptionMode -- [incognito] send membership incognito profile, create direct connection as incognito - let allowSimplexLinks = groupUserAllowSimplexLinks gInfo - membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership + membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile -- [async agent commands] no continuation needed, but commands should be asynchronous for stability groupConnIds <- joinAgentConnectionAsync user Nothing (chatHasNtfs chatSettings) groupConnReq dm subMode @@ -3340,7 +3335,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- transfer record + its scratch file in one transaction (file owned by the transfer, keyed per source) rft@RcvFileTransfer {fileId} <- withStore $ \db -> do transferId <- liftIO $ createRosterTransfer db gInfo (groupMemberId' fromMember) newVer fileDigest (groupMemberId' author) brokerTs relayHdr - createRosterRcvFile db userId gInfo fromMember transferId sharedMsgId rosterFInv (Just IFMSent) (fromIntegral chSize) + createRosterRcvFile db userId gInfo fromMember transferId sharedMsgId rosterFInv (Just IFMSent) chSize -- accept the chat-item-free file before chunk 1 (FIFO before it) so chunk 1 isn't rejected on RFSNew -- transient scratch file (consumed into roster_blob, then deleted): temp folder, not the user's files folder / Downloads tmpDir <- lift getChatTempDirectory @@ -3366,10 +3361,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Just RcvRosterTransfer {rosterTransferId = transferId, rosterTransferVersion = pendingVer, rosterTransferDigest = pendingDigest, rosterTransferOwnerGMId = ownerGMId, rosterTransferBrokerTs = rosterBrokerTs, rosterTransferHeader = header_} -> do owner_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberById db cxt user ownerGMId) blob <- readAssembledRoster - let isRelay = isUserGrpFwdRelay gInfo + let isRelay' = isUserGrpFwdRelay gInfo ackErr err = do cleanupRosterTransferById transferId - when isRelay $ forM_ owner_ $ \owner -> sendRosterAck gInfo owner pendingVer (Just err) + when isRelay' $ forM_ owner_ $ \owner -> sendRosterAck gInfo owner pendingVer (Just err) if FD.FileDigest (LC.sha512Hash (LB.fromStrict blob)) /= pendingDigest then ackErr "relay could not verify the roster blob" else case parseAll rosterBlobP blob of @@ -3393,7 +3388,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = forM_ results_ $ \results -> do emitRosterResults gInfo author rosterBrokerTs results -- ack while setting up (own status accepted/acknowledged); a serving (active) relay must not ack broadcasts. - when (isRelay && (relayOwnStatus gInfo == Just RSAccepted || relayOwnStatus gInfo == Just RSAcknowledgedRoster)) $ do + when (isRelay' && (relayOwnStatus gInfo == Just RSAccepted || relayOwnStatus gInfo == Just RSAcknowledgedRoster)) $ do sendRosterAck gInfo author pendingVer Nothing withStore' $ \db -> void $ updateRelayOwnStatusFromTo db gInfo RSAccepted RSAcknowledgedRoster where diff --git a/src/Simplex/Chat/Names.hs b/src/Simplex/Chat/Names.hs new file mode 100644 index 0000000000..081d7129a5 --- /dev/null +++ b/src/Simplex/Chat/Names.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Chat.Names + ( SimplexDomainClaim (..), + SimplexDomainProof (..), + mkDomainClaim, + claimDomain, + ) +where + +import qualified Data.Aeson.TH as JQ +import Simplex.Chat.Badges (ProofPresHeader) +import Simplex.Messaging.Agent.Protocol (OwnerId, SimplexDomain) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple.FromField (FromField (..)) +import Database.PostgreSQL.Simple.ToField (ToField (..)) +#else +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +#endif + +-- A name claim proof: signed by the address owner's key over proof payload - see verifyDomainProof. +data SimplexDomainProof = SimplexDomainProof + { linkOwnerId :: Maybe (StrJSON "OwnerId" OwnerId), + presHeader :: ProofPresHeader, + signature :: C.Signature 'C.Ed25519 + } + deriving (Eq, Show) + +$(JQ.deriveJSON defaultJSON ''SimplexDomainProof) + +instance ToField SimplexDomainProof where toField = toField . encodeJSON + +instance FromField SimplexDomainProof where fromField = fromTextField_ decodeJSON + +data SimplexDomainClaim = SimplexDomainClaim + { domain :: StrJSON "SimplexDomain" SimplexDomain, + proof :: Maybe SimplexDomainProof + } + deriving (Eq, Show) + +mkDomainClaim :: SimplexDomain -> SimplexDomainClaim +mkDomainClaim = (`SimplexDomainClaim` Nothing) . StrJSON + +claimDomain :: SimplexDomainClaim -> SimplexDomain +claimDomain (SimplexDomainClaim n _) = unStrJSON n + +$(JQ.deriveJSON defaultJSON ''SimplexDomainClaim) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 6816a5f692..ad03529613 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -511,7 +511,9 @@ data UserServersError | USEDuplicateChatRelayAddress {duplicateChatRelay :: Text, duplicateAddress :: ShortLinkContact} deriving (Show) -data UserServersWarning = USWNoChatRelays {user :: Maybe User} +data UserServersWarning + = USWNoChatRelays {user :: Maybe User} + | USWNoNamesServers {user :: Maybe User} deriving (Show) validateUserServers :: UserServersClass u' => [u'] -> [(User, [UserOperatorServers])] -> ([UserServersError], [UserServersWarning]) @@ -552,15 +554,19 @@ validateUserServers curr others = (currUserErrs <> concatMap otherUserErrs other addAddress (xs, dups) x | any (sameShortLinkContact x) xs = (xs, x : dups) | otherwise = (x : xs, dups) - currUserWarns = noChatRelaysWarns Nothing curr - otherUserWarns (user, uss) = noChatRelaysWarns (Just user) uss + currUserWarns = noChatRelaysWarns Nothing curr <> noNamesServersWarns Nothing curr + otherUserWarns (user, uss) = noChatRelaysWarns (Just user) uss <> noNamesServersWarns (Just user) uss noChatRelaysWarns :: UserServersClass u => Maybe User -> [u] -> [UserServersWarning] - noChatRelaysWarns user uss - | noChatRelays opEnabled = [USWNoChatRelays user] - | otherwise = [] + noChatRelaysWarns user uss = [USWNoChatRelays user | noChatRelays opEnabled] where noChatRelays cond = not $ any relayEnabled $ userChatRelays $ filter cond uss relayEnabled (AUCR _ UserChatRelay {deleted, enabled}) = enabled && not deleted + noNamesServersWarns :: UserServersClass u => Maybe User -> [u] -> [UserServersWarning] + noNamesServersWarns user uss = [USWNoNamesServers user | noNamesServers] + where + noNamesServers = not $ any srvEnabled $ userServers SPSMP $ filter namesEnabled uss + srvEnabled (AUS _ UserServer {deleted, enabled}) = enabled && not deleted + namesEnabled = maybe True (\op@ServerOperator {enabled} -> enabled && names (operatorRoles SPSMP op)) . operator' userChatRelays :: UserServersClass u => [u] -> [AUserChatRelay] userChatRelays = map aUserChatRelay' . concatMap chatRelays' opEnabled :: UserServersClass u => u -> Bool diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs index 53f31e005d..d55d23e2b9 100644 --- a/src/Simplex/Chat/Operators/Presets.hs +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -39,8 +39,8 @@ operatorFlux = serverDomains = ["simplexonflux.com"], conditionsAcceptance = CARequired Nothing, enabled = True, - smpRoles = ServerRoles {storage = False, proxy = True}, - xftpRoles = ServerRoles {storage = False, proxy = True} + smpRoles = ServerRoles {storage = False, proxy = True, names = True}, + xftpRoles = allRoles } -- Please note: if any servers are removed from the lists below, they MUST be added here. diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs index 7d272481f6..4d10945ab6 100644 --- a/src/Simplex/Chat/ProfileGenerator.hs +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -10,7 +10,7 @@ generateRandomProfile :: IO Profile generateRandomProfile = do adjective <- pick adjectives noun <- pickNoun adjective 2 - pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing} + pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} where pick :: [a] -> IO a pick xs = (xs !!) <$> randomRIO (0, length xs - 1) diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 2953c1de1f..74d574b643 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -116,15 +116,16 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? AND c.contact_status = ? AND c.deleted = 0 |] (userId, contactId, CSActive) toContact' :: UTCTime -> Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact - toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow) = - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} + toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) = + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn @@ -143,26 +144,26 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, 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.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, 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, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link FROM group_members m diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index 9c5fe0cd91..3bd481d7db 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -116,7 +116,7 @@ createOrUpdateContactRequest cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, - cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -152,7 +152,7 @@ createOrUpdateContactRequest cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 5068c5c61c..71e5c80a63 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -46,10 +46,12 @@ module Simplex.Chat.Store.Direct deleteContactWithoutGroups, getDeletedContacts, getContactByName, + getContactToConnect, getContact, getContactViaShortLinkToConnect, getContactIdByName, updateContactProfile, + setContactDomainVerified, updateContactUserPreferences, updateContactAlias, updateContactConnectionAlias, @@ -98,6 +100,7 @@ where import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Data.Bifunctor (first) import Data.Either (rights) import Data.Functor (($>)) import Data.Int (Int64) @@ -108,16 +111,18 @@ import Data.Type.Equality import Simplex.Chat.Badges (badgeToRow) import Simplex.Chat.Messages import Simplex.Chat.Store.Shared +import Simplex.Chat.Names (SimplexDomainClaim (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionModeI (..), ConnectionRequestUri, CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionModeI (..), ConnectionRequestUri, CreatedConnLink (..), SConnectionMode (..), SimplexNameInfo (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Crypto.Ratchet (PQSupport, pattern PQSupportOff) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol (SubscriptionMode (..)) +import Simplex.Messaging.Util ((<$$>)) #if defined(dbPostgres) import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..)) import Database.PostgreSQL.Simple.SqlQQ (sql) @@ -321,6 +326,7 @@ getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -397,13 +403,13 @@ createIncognitoProfile db User {userId} p = do createdAt <- getCurrentTime createIncognitoProfile_ db userId createdAt p -createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> ExceptT StoreError IO Contact -createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId = do +createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe Bool -> ExceptT StoreError IO Contact +createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId verified_ = do currentTs <- liftIO getCurrentTime let prepared = Just (connLinkToConnect, welcomeSharedMsgId) ctUserPreferences = newContactUserPrefs user p - contactId <- createContact_ db cxt user p ctUserPreferences prepared "" currentTs - getContact db cxt user contactId + ct <- getContact db cxt user =<< createContact_ db cxt user p ctUserPreferences prepared "" currentTs + liftIO $ maybe (pure ct) (setContactDomainVerified db user ct) verified_ updatePreparedContactUser :: DB.Connection -> StoreCxt -> User -> Contact -> User -> ExceptT StoreError IO Contact updatePreparedContactUser @@ -559,22 +565,41 @@ updateContactProfile :: DB.Connection -> StoreCxt -> User -> Contact -> Profile updateContactProfile db cxt user@User {userId} c p' = do currentTs <- liftIO getCurrentTime badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) lp p' - let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified + let nameVerified = if claimChanged then Nothing else prevVerification + profile = toLocalProfile profileId p'' localAlias currentTs badgeVerified nameVerified updateContactProfile' currentTs badgeVerified profile where - Contact {contactId, localDisplayName, profile = lp@LocalProfile {profileId, displayName, localAlias}, userPreferences} = c - Profile {displayName = newName, preferences} = p' + Contact {contactId, localDisplayName, profile = lp@LocalProfile {profileId, displayName, localAlias, contactDomain = prevClaim, contactDomainVerified = prevVerification}, userPreferences} = c + Profile {displayName = newName, contactDomain, preferences} = p' mergedPreferences = contactUserPreferences user userPreferences preferences $ contactConnIncognito c + claimChanged = (domain <$> prevClaim) /= (domain <$> contactDomain) + p'' = (p' :: Profile) {contactDomain = (\d -> d {proof = if claimChanged then Nothing else proof =<< prevClaim}) <$> contactDomain} + clearVerificationIfClaimChanged = + when claimChanged $ + DB.execute db "UPDATE contact_profiles SET contact_domain_verified = NULL WHERE user_id = ? AND contact_profile_id = ?" (userId, profileId) updateContactProfile' currentTs badgeVerified profile | displayName == newName = do - liftIO $ updateContactProfile_' db userId profileId p' badgeVerified currentTs + liftIO $ updateContactProfile_' db userId profileId p'' badgeVerified currentTs + liftIO clearVerificationIfClaimChanged pure c {profile, mergedPreferences} | otherwise = ExceptT . withLocalDisplayName db userId newName $ \ldn -> do - updateContactProfile_' db userId profileId p' badgeVerified currentTs + updateContactProfile_' db userId profileId p'' badgeVerified currentTs updateContactLDN_ db user contactId localDisplayName ldn currentTs + clearVerificationIfClaimChanged pure $ Right c {localDisplayName = ldn, profile, mergedPreferences} +setContactDomainVerified :: DB.Connection -> User -> Contact -> Bool -> IO Contact +setContactDomainVerified db User {userId} ct@Contact {contactId, profile = p} verified = do + DB.execute + db + [sql| + UPDATE contact_profiles SET contact_domain_verified = ? + WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?) + |] + (BI verified, userId, contactId) + pure (ct {profile = p {contactDomainVerified = Just verified}} :: Contact) + updateContactUserPreferences :: DB.Connection -> User -> Contact -> Preferences -> IO Contact updateContactUserPreferences db user@User {userId} c@Contact {contactId} userPreferences = do updatedAt <- getCurrentTime @@ -706,16 +731,17 @@ updateContactProfile_ db userId profileId profile badgeVerified = do updateContactProfile_' db userId profileId profile badgeVerified currentTs updateContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge} badgeVerified updatedAt = +updateContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain, preferences, peerType, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId)) + ((displayName, fullName, shortDescr, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) -- update only member profile fields (when member doesn't have associated contact - we can reset contactLink and prefs) updateMemberContactProfileReset_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO () @@ -724,16 +750,17 @@ updateMemberContactProfileReset_ db userId profileId profile badgeVerified = do updateMemberContactProfileReset_' db userId profileId profile badgeVerified currentTs updateMemberContactProfileReset_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, image, badge} badgeVerified updatedAt = +updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactDomain, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId)) + ((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) -- update only member profile fields (when member has associated contact - we keep contactLink and prefs) updateMemberContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO () @@ -742,16 +769,17 @@ updateMemberContactProfile_ db userId profileId profile badgeVerified = do updateMemberContactProfile_' db userId profileId profile badgeVerified currentTs updateMemberContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, badge} badgeVerified updatedAt = +updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactDomain, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId)) + ((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) updateContactLDN_ :: DB.Connection -> User -> Int64 -> ContactName -> ContactName -> UTCTime -> IO () updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt = do @@ -770,6 +798,22 @@ getContactByName db cxt user localDisplayName = do cId <- getContactIdByName db user localDisplayName getContact db cxt user cId +getContactToConnect :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> ExceptT StoreError IO (Maybe (CreatedLinkContact, Contact)) +getContactToConnect db cxt user@User {userId} = \case + CTLink sl -> first (`CCLink` Just sl) <$$> getContactViaShortLinkToConnect db cxt user sl + CTName ni -> + liftIO (maybeFirstRow id $ DB.query db byNameQuery (userId, nameDomain ni)) >>= \case + Just (ctId :: Int64, Just (ACR cMode cReq), Just (sLnk :: ShortLinkContact)) | Just Refl <- testEquality cMode SCMContact -> + Just . (CCLink cReq (Just sLnk),) <$> getContact db cxt user ctId + _ -> pure Nothing + where + byNameQuery = + [sql| + SELECT ct.contact_id, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect FROM contacts ct + JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id + WHERE ct.user_id = ? AND cp.contact_domain = ? AND cp.contact_domain_verified = 1 AND ct.deleted = 0 + |] + getUserContacts :: DB.Connection -> StoreCxt -> User -> IO [Contact] getUserContacts db cxt user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId) @@ -809,7 +853,8 @@ contactRequestQuery = cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) |] @@ -930,6 +975,7 @@ getContact_ db cxt user@User {userId} contactId deleted = do ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 60531cc1dc..7ed599007c 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -42,16 +42,18 @@ module Simplex.Chat.Store.Groups setGroupInvitationChatItemId, getGroup, getGroupInfoByUserContactLinkConnReq, - getGroupInfoViaUserShortLink, + getGroupInfoViaUserTarget, getGroupViaShortLinkToConnect, getGroupInfoByGroupLinkHash, updateGroupProfile, + setGroupDomainVerified, updateGroupPreferences, updateGroupProfileFromMember, getGroupIdByName, getGroupMemberIdByName, getActiveMembersByName, getGroupInfoByName, + getGroupToConnect, getGroupMember, getHostMember, getMentionedGroupMember, @@ -205,7 +207,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) -import Data.Bifunctor (second) +import Data.Bifunctor (first, second) import Data.ByteString (ByteString) import qualified Data.ByteString as B import Data.Char (toLower) @@ -220,6 +222,7 @@ import qualified Data.Text as T import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, getCurrentTime) import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Badges (BadgeRow, badgeToRow, verifyBadge_) +import Simplex.Chat.Names (SimplexDomainClaim (..)) import Simplex.Chat.Messages import Simplex.Chat.Operators import Simplex.Chat.Protocol hiding (Binary) @@ -230,7 +233,7 @@ import Simplex.Chat.Types.MemberRelations (IntroductionDirection (..), MemberRel import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), UserId) +import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), SimplexNameInfo (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) import qualified Simplex.FileTransfer.Description as FD import Simplex.Messaging.Encoding (smpDecode, smpEncode) @@ -251,11 +254,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact) +type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact) toMaybeGroupMember :: UTCTime -> Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) = - Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) +toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) = + Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) toMaybeGroupMember _ _ _ = Nothing createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink @@ -395,9 +398,9 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, - group_web_page, group_domain, domain_web_page, allow_embedding, + group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup :. (userId, groupPreferences, memberAdmission, currentTs, currentTs)) @@ -443,7 +446,8 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays customData = Nothing, membersRequireAttention = 0, viaGroupLinkUri = Nothing, - groupKeys + groupKeys, + groupDomainVerified = Nothing } -- | creates a new group record for the group the current user was invited to, or returns an existing one @@ -521,7 +525,8 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti customData = Nothing, membersRequireAttention = 0, viaGroupLinkUri = Nothing, - groupKeys = Nothing + groupKeys = Nothing, + groupDomainVerified = Nothing }, groupMemberId ) @@ -637,8 +642,8 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId) DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) -createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> GroupMemberRole -> Maybe Int64 -> ExceptT StoreError IO (GroupInfo, Maybe GroupMember) -createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays userMemberRole publicMemberCount_ = do +createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> GroupMemberRole -> Maybe Int64 -> Maybe Bool -> ExceptT StoreError IO (GroupInfo, Maybe GroupMember) +createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays userMemberRole publicMemberCount_ verified_ = do currentTs <- liftIO getCurrentTime let prepared = Just (connLinkToConnect, welcomeSharedMsgId) (groupId, groupLDN) <- createGroup_ db userId groupProfile prepared Nothing useRelays Nothing publicMemberCount_ currentTs @@ -657,7 +662,8 @@ createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile b forM_ hostMember_ $ \hostMember -> when business $ liftIO $ setGroupBusinessChatInfo groupId membership hostMember g <- getGroupInfo db cxt user groupId - pure (g, hostMember_) + g' <- liftIO $ maybe (pure g) (setGroupDomainVerified db user g) verified_ + pure (g', hostMember_) where insertHost_ currentTs groupId groupLDN = do randHostId <- liftIO $ encodedRandomBytes gVar 12 @@ -899,9 +905,9 @@ createGroup_ db userId groupProfile prepared business useRelays relayOwnStatus p INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, - group_web_page, group_domain, domain_web_page, allow_embedding, + group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup :. (userId, groupPreferences, memberAdmission, currentTs, currentTs)) @@ -1068,6 +1074,21 @@ getGroupInfoByName db cxt user gName = do gId <- getGroupIdByName db user gName getGroupInfo db cxt user gId +getGroupToConnect :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> ExceptT StoreError IO (Maybe (CreatedLinkContact, GroupInfo)) +getGroupToConnect db cxt user@User {userId} = \case + CTLink sl -> first (`CCLink` Just sl) <$$> getGroupViaShortLinkToConnect db cxt user sl + CTName ni -> + liftIO (maybeFirstRow id $ DB.query db byNameQuery (userId, nameDomain ni)) >>= \case + Just (gId :: Int64, Just cReq, Just (sLnk :: ShortLinkContact)) -> Just . (CCLink cReq (Just sLnk),) <$> getGroupInfo db cxt user gId + _ -> pure Nothing + where + byNameQuery = + [sql| + SELECT g.group_id, g.conn_full_link_to_connect, g.conn_short_link_to_connect FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified = 1 + |] + getGroupMember :: DB.Connection -> StoreCxt -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMember db cxt user@User {userId} groupId groupMemberId = do currentTs <- liftIO getCurrentTime @@ -1929,7 +1950,7 @@ getRelayPublishableGroups db User {userId, userContactId} = db [sql| SELECT g.group_id, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id AND mu.contact_id = ? @@ -2432,7 +2453,7 @@ createNewMember_ invitedBy, invitedByGroupMemberId = memInvitedByGroupMemberId, localDisplayName, - memberProfile = toLocalProfile memberContactProfileId memberProfile "" createdAt badgeVerified, + memberProfile = toLocalProfile memberContactProfileId memberProfile "" createdAt badgeVerified Nothing, memberContactId, memberContactProfileId, activeConn, @@ -2619,19 +2640,27 @@ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo -updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} +updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, publicGroup = oldPublicGroup}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} | displayName == newName = liftIO $ do currentTs <- getCurrentTime updateGroupProfile_ currentTs - pure (g :: GroupInfo) {groupProfile = p', fullGroupPreferences} + clearVerificationIfClaimChanged + pure $ (g' :: GroupInfo) {groupProfile = p', fullGroupPreferences} | otherwise = ExceptT . withLocalDisplayName db userId newName $ \ldn -> do currentTs <- getCurrentTime updateGroupProfile_ currentTs updateGroup_ ldn currentTs - pure $ Right (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences} + clearVerificationIfClaimChanged + pure $ Right $ (g' :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences} where fullGroupPreferences = mergeGroupPreferences groupPreferences + groupClaim pg = domain <$> (pg >>= publicGroupAccess >>= groupDomainClaim) + claimChanged = groupClaim oldPublicGroup /= groupClaim publicGroup + g' = if claimChanged then (g :: GroupInfo) {groupDomainVerified = Nothing} else g + clearVerificationIfClaimChanged = + when claimChanged $ + DB.execute db "UPDATE groups SET group_domain_verified = NULL WHERE user_id = ? AND group_id = ?" (userId, groupId) (groupType_, groupLink_) = case publicGroup of Just PublicGroupProfile {groupType, groupLink} -> (Just groupType, Just groupLink) Nothing -> (Nothing, Nothing) @@ -2642,7 +2671,7 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, UPDATE group_profiles SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, group_type = ?, group_link = ?, - group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?, + group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?, preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id @@ -2658,6 +2687,14 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, (ldn, currentTs, userId, groupId) safeDeleteLDN db user localDisplayName +setGroupDomainVerified :: DB.Connection -> User -> GroupInfo -> Bool -> IO GroupInfo +setGroupDomainVerified db User {userId} g@GroupInfo {groupId} verified = do + DB.execute + db + "UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ?" + (BI verified, userId, groupId) + pure g {groupDomainVerified = Just verified} + updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do currentTs <- getCurrentTime @@ -2690,7 +2727,7 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName [sql| SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id @@ -2716,24 +2753,36 @@ getGroupInfoByUserContactLinkConnReq db cxt user@User {userId} (cReqSchema1, cRe (userId, cReqSchema1, cReqSchema2) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db cxt user) groupId_ -getGroupInfoViaUserShortLink :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> IO (Maybe (ConnReqContact, GroupInfo)) -getGroupInfoViaUserShortLink db cxt user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do - (cReq, groupId) <- ExceptT getConnReqGroup - (cReq,) <$> getGroupInfo db cxt user groupId +getGroupInfoViaUserTarget :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> IO (Maybe (CreatedLinkContact, GroupInfo)) +getGroupInfoViaUserTarget db cxt user@User {userId} target = fmap eitherToMaybe $ runExceptT $ do + (cReq, sLnk, groupId) <- ExceptT getConnReqGroup + (CCLink cReq (Just sLnk),) <$> getGroupInfo db cxt user groupId where getConnReqGroup = - firstRow' toConnReqGroupId (SEInternalError "group link not found") $ - DB.query - db - [sql| - SELECT conn_req_contact, group_id - FROM user_contact_links - WHERE user_id = ? AND short_link_contact = ? - |] - (userId, shortLink) + firstRow' toConnReqGroupId (SEInternalError "group link not found") $ case target of + CTLink shortLink -> + DB.query + db + [sql| + SELECT conn_req_contact, short_link_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? + |] + (userId, shortLink) + CTName ni -> + DB.query + db + [sql| + SELECT ucl.conn_req_contact, ucl.short_link_contact, ucl.group_id + FROM user_contact_links ucl + JOIN groups g ON g.group_id = ucl.group_id + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE ucl.user_id = ? AND gp.group_domain = ? + |] + (userId, nameDomain ni) toConnReqGroupId = \case -- cReq is "not null", group_id is nullable - (cReq, Just groupId) -> Right (cReq, groupId) + (cReq, Just (sLnk :: ShortLinkContact), Just groupId) -> Right (cReq, sLnk, groupId) _ -> Left $ SEInternalError "no conn req or group ID" getGroupViaShortLinkToConnect :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> ExceptT StoreError IO (Maybe (ConnReqContact, GroupInfo)) @@ -3286,7 +3335,7 @@ updateMemberProfile :: DB.Connection -> StoreCxt -> User -> GroupMember -> Profi updateMemberProfile db cxt user@User {userId} m p' = do currentTs <- liftIO getCurrentTime badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) (memberProfile m) p' - let memberProfile = toLocalProfile profileId p' localAlias currentTs badgeVerified + let memberProfile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing updateMemberProfile' currentTs badgeVerified memberProfile where GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m @@ -3309,7 +3358,7 @@ updateContactMemberProfile :: DB.Connection -> StoreCxt -> User -> GroupMember - updateContactMemberProfile db cxt user@User {userId} m ct@Contact {contactId} p' = do currentTs <- liftIO getCurrentTime badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) (memberProfile m) p' - let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified + let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing updateContactMemberProfile' currentTs badgeVerified profile where GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 644d73137d..3ddc04253a 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -715,7 +715,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link FROM group_members m @@ -1139,7 +1139,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = do cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id @@ -3070,7 +3070,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, -- quoted ChatItem @@ -3079,14 +3079,14 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, - rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, + rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified, rm.created_at, rm.updated_at, rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, - dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, + dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified, dbm.created_at, dbm.updated_at, dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link FROM chat_items i diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 4b814d0434..e17129cd1b 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -38,6 +38,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at import Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain import Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster +import Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -75,7 +76,8 @@ schemaMigrations = ("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services), ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), - ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster) + ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster), + ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260603_simplex_name.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260603_simplex_name.hs new file mode 100644 index 0000000000..14a0ae2a05 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260603_simplex_name.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260603_simplex_name :: Text +m20260603_simplex_name = + [r| +ALTER TABLE contact_profiles ADD COLUMN contact_domain TEXT; +ALTER TABLE contact_profiles ADD COLUMN contact_domain_proof TEXT; +ALTER TABLE contact_profiles ADD COLUMN contact_domain_verified SMALLINT; + +ALTER TABLE group_profiles ADD COLUMN group_domain_proof TEXT; +ALTER TABLE groups ADD COLUMN group_domain_verified SMALLINT; + +ALTER TABLE user_contact_links ADD COLUMN link_priv_sig_key BYTEA; + +ALTER TABLE server_operators ADD COLUMN smp_role_names SMALLINT NOT NULL DEFAULT 0; +UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex' OR server_operator_tag = 'flux'; +|] + +down_m20260603_simplex_name :: Text +down_m20260603_simplex_name = + [r| +ALTER TABLE contact_profiles DROP COLUMN contact_domain; +ALTER TABLE contact_profiles DROP COLUMN contact_domain_proof; +ALTER TABLE contact_profiles DROP COLUMN contact_domain_verified; + +ALTER TABLE group_profiles DROP COLUMN group_domain_proof; +ALTER TABLE groups DROP COLUMN group_domain_verified; + +ALTER TABLE user_contact_links DROP COLUMN link_priv_sig_key; + +ALTER TABLE server_operators DROP COLUMN smp_role_names; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 861224ff56..2799a79b81 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -540,7 +540,10 @@ CREATE TABLE test_chat_schema.contact_profiles ( badge_extra text, badge_master_key bytea, badge_signature bytea, - badge_key_idx bigint + badge_key_idx bigint, + contact_domain text, + contact_domain_proof text, + contact_domain_verified smallint ); @@ -866,7 +869,8 @@ CREATE TABLE test_chat_schema.group_profiles ( group_web_page text, group_domain text, domain_web_page bigint, - allow_embedding bigint + allow_embedding bigint, + group_domain_proof text ); @@ -980,7 +984,7 @@ CREATE TABLE test_chat_schema.groups ( public_member_count bigint, relay_request_retries bigint DEFAULT 0 NOT NULL, relay_request_delay bigint DEFAULT 0 NOT NULL, - relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 04:00:00+04'::timestamp with time zone NOT NULL, + relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 01:00:00+01'::timestamp with time zone NOT NULL, relay_inactive_at timestamp with time zone, relay_sent_web_domain text, roster_version bigint, @@ -989,7 +993,8 @@ CREATE TABLE test_chat_schema.groups ( roster_msg_signatures bytea, roster_sending_owner_gm_id bigint, roster_broker_ts timestamp with time zone, - roster_blob bytea + roster_blob bytea, + group_domain_verified smallint ); @@ -1377,7 +1382,8 @@ CREATE TABLE test_chat_schema.server_operators ( xftp_role_storage smallint DEFAULT 1 NOT NULL, xftp_role_proxy smallint DEFAULT 1 NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL + updated_at timestamp with time zone DEFAULT now() NOT NULL, + smp_role_names smallint DEFAULT 0 NOT NULL ); @@ -1454,7 +1460,8 @@ CREATE TABLE test_chat_schema.user_contact_links ( business_address smallint DEFAULT 0, short_link_contact bytea, short_link_data_set smallint DEFAULT 0 NOT NULL, - short_link_large_data_set smallint DEFAULT 0 NOT NULL + short_link_large_data_set smallint DEFAULT 0 NOT NULL, + link_priv_sig_key bytea ); diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 043897a995..522f966214 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -50,10 +50,11 @@ module Simplex.Chat.Store.Profiles getUserAddressConnection, deleteUserAddress, getUserAddress, + setUserSimplexDomain, getUserContactLinkById, getGroupLinkInfo, getUserContactLinkByConnReq, - getUserContactLinkViaShortLink, + getUserContactLinkViaTarget, setUserContactLinkShortLink, getContactWithoutConnViaAddress, getContactWithoutConnViaShortAddress, @@ -105,12 +106,13 @@ import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared +import Simplex.Chat.Names (claimDomain, mkDomainClaim) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) -import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), SimplexDomain, SimplexNameInfo (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -161,7 +163,7 @@ createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {di (profileId, displayName, userId, BI True, currentTs, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing + pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing :. (Nothing, Nothing, Nothing) -- TODO [mentions] getUsersInfo :: DB.Connection -> IO [UserInfo] @@ -332,7 +334,7 @@ updateUserProfile db user p' currentTs <- getCurrentTime updateUserProfileFields_' db userId profileId p' currentTs userMemberProfileUpdatedAt' <- updateUserMemberProfileUpdatedAt_ currentTs - pure user {profile = (toLocalProfile profileId p' localAlias currentTs (Just False)) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} + pure user {profile = (toLocalProfile profileId p' localAlias currentTs (Just False) Nothing) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} | otherwise = checkConstraint SEDuplicateName . liftIO $ do currentTs <- getCurrentTime @@ -344,7 +346,7 @@ updateUserProfile db user p' (newName, newName, userId, currentTs, currentTs) updateUserProfileFields_' db userId profileId p' currentTs updateContactLDN_ db user userContactId localDisplayName newName currentTs - pure user {localDisplayName = newName, profile = (toLocalProfile profileId p' localAlias currentTs (Just False)) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} + pure user {localDisplayName = newName, profile = (toLocalProfile profileId p' localAlias currentTs (Just False) Nothing) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} where updateUserMemberProfileUpdatedAt_ currentTs | userMemberProfileChanged = do @@ -384,6 +386,15 @@ setUserBadge db user@User {userId, profile = p@LocalProfile {profileId}} localBa DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (ts, userId) pure (user :: User) {profile = p {localBadge}, userMemberProfileUpdatedAt = Just ts} +setUserSimplexDomain :: DB.Connection -> User -> Maybe SimplexDomain -> IO User +setUserSimplexDomain db user@User {userId, profile = p@LocalProfile {profileId}} domain_ = do + ts <- getCurrentTime + DB.execute + db + "UPDATE contact_profiles SET contact_domain = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ?" + (domain_, ts, userId, profileId) + pure (user :: User) {profile = p {contactDomain = mkDomainClaim <$> domain_}} + setUserProfileContactLink :: DB.Connection -> User -> Maybe UserContactLink -> IO User setUserProfileContactLink db user@User {userId, profile = p@LocalProfile {profileId}} ucl_ = do ts <- getCurrentTime @@ -406,24 +417,24 @@ getUserContactProfiles db User {userId} = <$> DB.query db [sql| - SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, preferences + SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, contact_domain, preferences FROM contact_profiles WHERE user_id = ? |] (Only userId) where - toContactProfile :: (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) -> Profile - toContactProfile (displayName, fullName, shortDescr, image, contactLink, peerType, preferences) = Profile {displayName, fullName, shortDescr, image, contactLink, peerType, preferences, badge = Nothing} + toContactProfile :: (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe SimplexDomain, Maybe Preferences) -> Profile + toContactProfile (displayName, fullName, shortDescr, image, contactLink, peerType, domain_, preferences) = Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain = mkDomainClaim <$> domain_, peerType, preferences, badge = Nothing} -createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> ExceptT StoreError IO () -createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode = +createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> C.PrivateKeyEd25519 -> ExceptT StoreError IO () +createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode linkPrivSigKey = checkConstraint SEDuplicateContactLink . liftIO $ do currentTs <- getCurrentTime let slDataSet = BI (isJust shortLink) DB.execute db - "INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (userId, cReq, shortLink, slDataSet, slDataSet, currentTs, currentTs) + "INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, link_priv_sig_key, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (userId, cReq, shortLink, slDataSet, slDataSet, linkPrivSigKey, currentTs, currentTs) userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff @@ -546,10 +557,16 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = maybeFirstRow toUserContactLink $ DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND conn_req_contact IN (?,?)") (userId, cReqSchema1, cReqSchema2) -getUserContactLinkViaShortLink :: DB.Connection -> User -> ShortLinkContact -> IO (Maybe UserContactLink) -getUserContactLinkViaShortLink db User {userId} shortLink = - maybeFirstRow toUserContactLink $ - DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink) +getUserContactLinkViaTarget :: DB.Connection -> User -> ContactNameOrLink -> IO (Maybe UserContactLink) +getUserContactLinkViaTarget db User {userId, profile = LocalProfile {contactDomain}} = \case + CTLink shortLink -> + maybeFirstRow toUserContactLink $ + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink) + CTName ni + | (claimDomain <$> contactDomain) == Just (nameDomain ni) -> + maybeFirstRow toUserContactLink $ + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND group_id IS NULL AND short_link_contact IS NOT NULL") (Only userId) + | otherwise -> pure Nothing userContactLinkQuery :: Query userContactLinkQuery = @@ -752,10 +769,10 @@ updateServerOperator db currentTs ServerOperator {operatorId, enabled, smpRoles, db [sql| UPDATE server_operators - SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? + SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? WHERE server_operator_id = ? |] - (BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId) + (BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId) getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [(Maybe PresetOperator, Maybe ServerOperator)] getUpdateServerOperators db presetOps newUser = do @@ -790,20 +807,20 @@ getUpdateServerOperators db presetOps newUser = do db [sql| UPDATE server_operators - SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ? + SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ? WHERE server_operator_id = ? |] - (tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId) + (tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId) insertOperator :: NewServerOperator -> IO ServerOperator insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = do DB.execute db [sql| INSERT INTO server_operators - (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy) - VALUES (?,?,?,?,?,?,?,?,?) + (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy) + VALUES (?,?,?,?,?,?,?,?,?,?) |] - (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles)) + (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles)) opId <- insertedRowId db pure op {operatorId = DBEntityId opId} autoAcceptConditions op UsageConditions {conditionsCommit} now = @@ -814,14 +831,14 @@ serverOperatorQuery :: Query serverOperatorQuery = [sql| SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy FROM server_operators |] getServerOperators_ :: DB.Connection -> IO [ServerOperator] getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery -toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator +toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI enabled) :. smpRoles' :. xftpRoles') = ServerOperator { operatorId, @@ -831,11 +848,12 @@ toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI en serverDomains = T.splitOn "," domains, conditionsAcceptance = CARequired Nothing, enabled, - smpRoles = serverRoles smpRoles', - xftpRoles = serverRoles xftpRoles' + smpRoles = serverRolesSMP smpRoles', + xftpRoles = serverRolesXFTP xftpRoles' } where - serverRoles (BI storage, BI proxy) = ServerRoles {storage, proxy} + serverRolesSMP (BI storage, BI proxy, BI names) = ServerRoles {storage, proxy, names} + serverRolesXFTP (BI storage, BI proxy) = ServerRoles {storage, proxy, names = False} getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index c9dd316aee..0cf78db595 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -161,6 +161,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at import Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain import Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster +import Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -321,7 +322,8 @@ schemaMigrations = ("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services), ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), - ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster) + ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster), + ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260603_simplex_name.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260603_simplex_name.hs new file mode 100644 index 0000000000..112f06a0a1 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260603_simplex_name.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260603_simplex_name :: Query +m20260603_simplex_name = + [sql| +ALTER TABLE contact_profiles ADD COLUMN contact_domain TEXT; +ALTER TABLE contact_profiles ADD COLUMN contact_domain_proof TEXT; +ALTER TABLE contact_profiles ADD COLUMN contact_domain_verified INTEGER; + +ALTER TABLE group_profiles ADD COLUMN group_domain_proof TEXT; +ALTER TABLE groups ADD COLUMN group_domain_verified INTEGER; + +ALTER TABLE user_contact_links ADD COLUMN link_priv_sig_key BLOB; + +ALTER TABLE server_operators ADD COLUMN smp_role_names INTEGER NOT NULL DEFAULT 0; +UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex' OR server_operator_tag = 'flux'; +|] + +down_m20260603_simplex_name :: Query +down_m20260603_simplex_name = + [sql| +ALTER TABLE contact_profiles DROP COLUMN contact_domain; +ALTER TABLE contact_profiles DROP COLUMN contact_domain_proof; +ALTER TABLE contact_profiles DROP COLUMN contact_domain_verified; + +ALTER TABLE group_profiles DROP COLUMN group_domain_proof; +ALTER TABLE groups DROP COLUMN group_domain_verified; + +ALTER TABLE user_contact_links DROP COLUMN link_priv_sig_key; + +ALTER TABLE server_operators DROP COLUMN smp_role_names; +|] 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 eefcb8de57..af2fb0162d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -125,7 +125,7 @@ Query: cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, - cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -144,26 +144,26 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, 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.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, 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, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link FROM group_members m @@ -401,7 +401,7 @@ Query: cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? @@ -443,6 +443,14 @@ Query: Plan: SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_server_operator_id (server_operator_id=?) +Query: + SELECT conn_req_contact, short_link_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? + +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) + Query: SELECT timed_ttl FROM chat_items @@ -451,6 +459,18 @@ Query: Plan: SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT ucl.conn_req_contact, ucl.short_link_contact, ucl.group_id + FROM user_contact_links ucl + JOIN groups g ON g.group_id = ucl.group_id + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE ucl.user_id = ? AND gp.group_domain = ? + +Plan: +SEARCH ucl USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE chat_items SET user_id = ?, updated_at = ? @@ -687,7 +707,8 @@ Query: p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? AND c.contact_status = ? AND c.deleted = 0 @@ -907,14 +928,6 @@ Plan: SEARCH chat_items USING INDEX idx_chat_items_groups_item_viewed (user_id=?) USE TEMP B-TREE FOR ORDER BY -Query: - SELECT conn_req_contact, group_id - FROM user_contact_links - WHERE user_id = ? AND short_link_contact = ? - -Plan: -SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) - Query: SELECT conn_req_contact, group_id FROM user_contact_links @@ -994,7 +1007,7 @@ SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_next (group_id=? A Query: SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id @@ -1038,7 +1051,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link FROM group_members m @@ -1269,9 +1282,9 @@ Query: INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, - group_web_page, group_domain, domain_web_page, allow_embedding, + group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1303,8 +1316,8 @@ Plan: Query: INSERT INTO server_operators - (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy) - VALUES (?,?,?,?,?,?,?,?,?) + (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy) + VALUES (?,?,?,?,?,?,?,?,?,?) Plan: @@ -1347,7 +1360,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, -- quoted ChatItem @@ -1356,14 +1369,14 @@ Query: rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, - rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, + rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified, rm.created_at, rm.updated_at, rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, - dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, + dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified, dbm.created_at, dbm.updated_at, dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link FROM chat_items i @@ -1417,6 +1430,7 @@ Query: ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -1810,7 +1824,7 @@ Query: UPDATE group_profiles SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, group_type = ?, group_link = ?, - group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?, + group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?, preferences = ?, member_admission = ?, updated_at = ? WHERE group_profile_id IN ( SELECT group_profile_id @@ -1997,6 +2011,7 @@ Query: ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -2082,7 +2097,7 @@ Query: cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id @@ -2112,7 +2127,7 @@ Query: cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id @@ -2142,7 +2157,7 @@ Query: cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id @@ -3672,8 +3687,8 @@ Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, - cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx + SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences + cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified FROM contact_profiles cp WHERE cp.user_id = ? AND cp.contact_profile_id = ? @@ -3693,6 +3708,15 @@ Plan: SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT ct.contact_id, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect FROM contacts ct + JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id + WHERE ct.user_id = ? AND cp.contact_domain = ? AND cp.contact_domain_verified = 1 AND ct.deleted = 0 + +Plan: +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT d.file_descr_id, d.file_descr_text, d.file_descr_part_no, d.file_descr_complete FROM xftp_file_descriptions d @@ -3716,7 +3740,7 @@ SEARCH f USING PRIMARY KEY (file_id=?) SEARCH d USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, preferences + SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, contact_domain, preferences FROM contact_profiles WHERE user_id = ? @@ -3768,9 +3792,18 @@ Query: Plan: SEARCH files USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT g.group_id, g.conn_full_link_to_connect, g.conn_short_link_to_connect FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified = 1 + +Plan: +SEARCH g USING INDEX sqlite_autoindex_groups_2 (user_id=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT g.group_id, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id AND mu.contact_id = ? @@ -5110,7 +5143,8 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5119,7 +5153,8 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5128,7 +5163,8 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5142,6 +5178,15 @@ Query: Plan: SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE contact_profiles SET contact_domain_verified = ? + WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contacts SET contact_group_member_id = NULL, contact_grp_inv_sent = 0, updated_at = ? @@ -5385,7 +5430,7 @@ SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE server_operators - SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? + SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? WHERE server_operator_id = ? Plan: @@ -5468,19 +5513,19 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, 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.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link @@ -5506,19 +5551,19 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, 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.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link @@ -5537,19 +5582,19 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, 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.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link @@ -5572,7 +5617,8 @@ Query: cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) WHERE cr.business_group_id = ? @@ -5588,7 +5634,8 @@ Query: cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? AND cr.contact_request_id = ? @@ -5600,7 +5647,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5628,7 +5675,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5649,7 +5696,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5669,7 +5716,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5689,7 +5736,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5709,7 +5756,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5729,7 +5776,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5749,7 +5796,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5769,7 +5816,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5789,7 +5836,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5809,7 +5856,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5829,7 +5876,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5849,7 +5896,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5869,7 +5916,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -5889,7 +5936,7 @@ Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -6073,7 +6120,7 @@ SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy FROM server_operators Plan: @@ -6081,7 +6128,7 @@ SCAN server_operators Query: SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy FROM server_operators WHERE server_operator_id = ? Plan: @@ -6090,7 +6137,7 @@ SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6103,7 +6150,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6117,7 +6164,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6131,7 +6178,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6146,7 +6193,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6160,7 +6207,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6174,7 +6221,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6188,7 +6235,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6202,7 +6249,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6215,7 +6262,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6848,7 +6895,7 @@ Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) -Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) +Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) @@ -6938,7 +6985,7 @@ Query: INSERT INTO snd_files (file_id, file_status, file_descr_id, group_member_ Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) -Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, created_at, updated_at) VALUES (?,?,?,?,?,?,?) +Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, link_priv_sig_key, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?) Plan: Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) @@ -7426,6 +7473,10 @@ Query: UPDATE connections_sync SET should_sync = 1 WHERE connections_sync_id = 1 Plan: SEARCH connections_sync USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE contact_profiles SET contact_domain = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contact_profiles SET image = ? WHERE display_name = ? Plan: SEARCH contact_profiles USING INDEX contact_profiles_index (display_name=?) @@ -7630,6 +7681,14 @@ Query: UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE use Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET group_domain_verified = NULL WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET local_alias = ?, updated_at = ? WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index d4d395c1dc..954660dce5 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -28,7 +28,10 @@ CREATE TABLE contact_profiles( badge_extra TEXT, badge_master_key BLOB, badge_signature BLOB, - badge_key_idx INTEGER + badge_key_idx INTEGER, + contact_domain TEXT, + contact_domain_proof TEXT, + contact_domain_verified INTEGER ) STRICT; CREATE TABLE users( user_id INTEGER PRIMARY KEY, @@ -139,7 +142,8 @@ CREATE TABLE group_profiles( group_web_page TEXT, group_domain TEXT, domain_web_page INTEGER, - allow_embedding INTEGER + allow_embedding INTEGER, + group_domain_proof TEXT ) STRICT; CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID @@ -199,7 +203,8 @@ CREATE TABLE groups( roster_msg_signatures BLOB, roster_sending_owner_gm_id INTEGER, roster_broker_ts TEXT, - roster_blob BLOB, -- received + roster_blob BLOB, + group_domain_verified INTEGER, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -393,6 +398,7 @@ CREATE TABLE user_contact_links( short_link_contact BLOB, short_link_data_set INTEGER NOT NULL DEFAULT 0, short_link_large_data_set INTEGER NOT NULL DEFAULT 0, + link_priv_sig_key BLOB, UNIQUE(user_id, local_display_name) ) STRICT; CREATE TABLE contact_requests( @@ -700,6 +706,8 @@ CREATE TABLE server_operators( xftp_role_proxy INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + smp_role_names INTEGER NOT NULL DEFAULT 0 ) STRICT; CREATE TABLE usage_conditions( usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 9096be3a86..0d01b2ac1d 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -33,13 +33,14 @@ import qualified Data.Text as T import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Type.Equality import Simplex.Chat.Badges (BadgeRow, badgeToRow, rowToBadge, verifyBadge_) +import Simplex.Chat.Names (SimplexDomainProof, SimplexDomainClaim (..), claimDomain) import Simplex.Chat.Messages import Simplex.Chat.Remote.Types import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), UserId, connMode) +import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), SimplexDomain, UserId, connMode) import Simplex.Messaging.Agent.Store (AnyStoreError (..)) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.Common (withSavepoint) @@ -48,6 +49,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..)) import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Encoding.String (StrJSON (..)) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (AnyError (..)) @@ -413,13 +415,13 @@ createContact db cxt user profile = do void $ createContact_ db cxt user profile emptyChatPrefs Nothing "" currentTs createContact_ :: DB.Connection -> StoreCxt -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> ExceptT StoreError IO ContactId -createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, peerType, badge, preferences} ctUserPreferences prepared localAlias currentTs = +createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain, peerType, badge, preferences} ctUserPreferences prepared localAlias currentTs = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do badgeVerified <- verifyBadge_ (badgeKeys cxt) badge DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain) profileId <- insertedRowId db DB.execute db @@ -486,13 +488,15 @@ type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Ma type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt) -type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow +type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow type ContactRow = Only ContactId :. ContactRow' +type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe BoolInt) + toContact :: UTCTime -> StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact -toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} +toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} activeConn = toMaybeConnection cxt connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn @@ -501,6 +505,15 @@ toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName groupDirectInv = toGroupDirectInvitation groupDirectInvRow in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} +rowToContactDomain :: ContactDomainRow -> Maybe SimplexDomainClaim +rowToContactDomain (domain_, domainProof_, _) = (`SimplexDomainClaim` domainProof_) . StrJSON <$> domain_ + +rowToDomainVerified :: ContactDomainRow -> Maybe Bool +rowToDomainVerified (_, _, domainVerification_) = unBI <$> domainVerification_ + +contactDomainToRow :: Maybe SimplexDomainClaim -> (Maybe SimplexDomain, Maybe SimplexDomainProof) +contactDomainToRow d = (claimDomain <$> d, proof =<< d) + toPreparedContact :: PreparedContactRow -> Maybe PreparedContact toPreparedContact (connFullLink, connShortLink, welcomeSharedMsgId, requestSharedMsgId) = (\cl@(ACCL m _) -> PreparedContact {connLinkToConnect = cl, uiConnLinkType = connMode m, welcomeSharedMsgId, requestSharedMsgId}) @@ -524,18 +537,18 @@ getProfileById db userId profileId = do DB.query db [sql| - SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, - cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx + SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences + cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified FROM contact_profiles cp WHERE cp.user_id = ? AND cp.contact_profile_id = ? |] (userId, profileId) -type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow toContactRequest :: UTCTime -> ContactRequestRow -> UserContactRequest -toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow) = do - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias} +toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias} cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt} @@ -544,17 +557,17 @@ userQuery = [sql| SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow -> User -toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow) = +toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow :. ContactDomainRow -> User +toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow :. domainRow) = User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, userChatRelay = BoolDef userChatRelay, clientService = BoolDef clientService, uiThemes} where - profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""} + profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""} fullPreferences = fullPreferences' userPreferences viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_ @@ -670,16 +683,16 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow -type PublicGroupAccessRow = (Maybe Text, Maybe Text, Maybe BoolInt, Maybe BoolInt) +type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof) type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact) -type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow +type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) = +toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) = let membership = (toGroupMember now userContactId userMemberRow) {memberChatVRange = vr cxt} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences @@ -689,7 +702,7 @@ toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayN businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers, publicMemberCount} - in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupKeys} + in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupKeys, groupDomainVerified = unBI <$> groupDomainVerified} toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup toPreparedGroup = \case @@ -704,14 +717,14 @@ toPublicGroupProfile _ _ _ _ = Nothing publicGroupAccessRow :: Maybe PublicGroupProfile -> PublicGroupAccessRow publicGroupAccessRow pgp = case pgp >>= publicGroupAccess of - Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding} -> - (groupWebPage, groupDomain, Just (BI domainWebPage), Just (BI allowEmbedding)) - Nothing -> (Nothing, Nothing, Nothing, Nothing) + Just PublicGroupAccess {groupWebPage, groupDomainClaim, domainWebPage, allowEmbedding} -> + (groupWebPage, claimDomain <$> groupDomainClaim, Just (BI domainWebPage), Just (BI allowEmbedding), proof =<< groupDomainClaim) + Nothing -> (Nothing, Nothing, Nothing, Nothing, Nothing) toPublicGroupAccess :: PublicGroupAccessRow -> Maybe PublicGroupAccess -toPublicGroupAccess (groupWebPage, groupDomain, domainWebPage_, allowEmbedding_) - | isJust groupWebPage || isJust groupDomain || domainWebPage || allowEmbedding = - Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding} +toPublicGroupAccess (groupWebPage, groupDomain_, domainWebPage_, allowEmbedding_, groupDomainProof_) + | isJust groupWebPage || isJust groupDomain_ || domainWebPage || allowEmbedding = + Just PublicGroupAccess {groupWebPage, groupDomainClaim = (`SimplexDomainClaim` groupDomainProof_) . StrJSON <$> groupDomain_, domainWebPage, allowEmbedding} | otherwise = Nothing where domainWebPage = maybe False unBI domainWebPage_ @@ -750,7 +763,7 @@ groupMemberQuery = SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, @@ -767,8 +780,8 @@ toContactMember now cxt User {userContactId} (memberRow :. connRow) = (toGroupMember now userContactId memberRow) {activeConn = toMaybeConnection cxt connRow} rowToLocalProfile :: UTCTime -> ProfileRow -> LocalProfile -rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, preferences) :. badgeRow) = - LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences} +rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, preferences) :. badgeRow :. domainRow) = + LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences} toBusinessChatInfo :: BusinessChatInfoRow -> Maybe BusinessChatInfo toBusinessChatInfo (Just chatType, Just businessId, Just customerId) = Just BusinessChatInfo {chatType, businessId, customerId} @@ -783,19 +796,19 @@ groupInfoQueryFields = SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, 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.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link |] diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 538faf8cac..25f8f378d2 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -4,6 +4,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} @@ -19,6 +20,7 @@ {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilyDependencies #-} +{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} @@ -34,6 +36,7 @@ import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import qualified Data.Attoparsec.ByteString.Char8 as A +import Data.Attoparsec.Combinator (lookAhead) import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString, pack, unpack) import qualified Data.ByteString.Char8 as B @@ -46,16 +49,18 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (UTCTime) +import Data.Type.Equality (testEquality, (:~:) (Refl)) import Data.Typeable (Typeable) import Data.Word (Word16) import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), localBadgeInfo, localBadgeStatus, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Names (SimplexDomainClaim (..)) import Simplex.Messaging.Crypto.BBS (BBSPublicKey) import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) -import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink, ConnectionMode (..), ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AConnectionLink (..), AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink (..), ConnectionMode (..), ConnectionModeI, ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), SConnectionMode (..), SimplexNameInfo, UserId) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) @@ -493,7 +498,8 @@ data GroupInfo = GroupInfo rosterVersion :: Maybe VersionRoster, membersRequireAttention :: Int, viaGroupLinkUri :: Maybe ConnReqContact, - groupKeys :: Maybe GroupKeys + groupKeys :: Maybe GroupKeys, + groupDomainVerified :: Maybe Bool } deriving (Eq, Show) @@ -641,12 +647,6 @@ groupFeatureUserAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupInfo - groupFeatureUserAllowed feature GroupInfo {membership = GroupMember {memberRole}, fullGroupPreferences} = groupFeatureMemberAllowed' feature memberRole fullGroupPreferences --- A connection link in a profile description enables a direct connection, so a description --- keeps its links only when both SimpleX links and direct messages are allowed. -groupUserAllowSimplexLinks :: GroupInfo -> Bool -groupUserAllowSimplexLinks g = - groupFeatureUserAllowed SGFSimplexLinks g && groupFeatureUserAllowed SGFDirectMessages g - mergeUserChatPrefs :: User -> Contact -> FullPreferences mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct) @@ -698,7 +698,8 @@ data Profile = Profile contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences, peerType :: Maybe ChatPeerType, - badge :: Maybe BadgeProof + badge :: Maybe BadgeProof, + contactDomain :: Maybe SimplexDomainClaim -- fields that should not be read into this data type to prevent sending them as part of profile to contacts: -- - contact_profile_id -- - incognito @@ -731,21 +732,22 @@ instance TextEncoding ChatPeerType where profileFromName :: ContactName -> Profile profileFromName displayName = - Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, preferences = Nothing, peerType = Nothing, badge = Nothing} + Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, preferences = Nothing, peerType = Nothing, badge = Nothing, contactDomain = Nothing} -- check if profiles match ignoring preferences profilesMatch :: LocalProfile -> LocalProfile -> Bool profilesMatch - LocalProfile {displayName = n1, fullName = fn1, image = i1} - LocalProfile {displayName = n2, fullName = fn2, image = i2} = - n1 == n2 && fn1 == fn2 && i1 == i2 + LocalProfile {displayName = n1, fullName = fn1, image = i1, shortDescr = d1} + LocalProfile {displayName = n2, fullName = fn2, image = i2, shortDescr = d2} = + n1 == n2 && fn1 == fn2 && i1 == i2 && d1 == d2 -- equal for profile-update detection: badge proofs are re-generated for every presentation, -- so compare badges by disclosed info (not proof bytes) - a re-presentation of the same badge is a no-op sameProfileContent :: Profile -> Profile -> Bool sameProfileContent p@Profile {badge = b} p'@Profile {badge = b'} = - p {badge = Nothing} == p' {badge = Nothing} && (proofInfo <$> b) == (proofInfo <$> b') + clearProofs p == clearProofs p' && (proofInfo <$> b) == (proofInfo <$> b') where + clearProofs pr@Profile {contactDomain} = pr {badge = Nothing, contactDomain = (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain} proofInfo :: BadgeProof -> BadgeInfo proofInfo (BadgeProof _ _ _ info) = info @@ -781,29 +783,31 @@ data LocalProfile = LocalProfile preferences :: Maybe Preferences, peerType :: Maybe ChatPeerType, localBadge :: Maybe LocalBadge, - localAlias :: LocalAlias + localAlias :: LocalAlias, + contactDomain :: Maybe SimplexDomainClaim, + contactDomainVerified :: Maybe Bool } deriving (Eq, Show) localProfileId :: LocalProfile -> ProfileId localProfileId LocalProfile {profileId} = profileId -toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> LocalProfile -toLocalProfile profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge} localAlias now verified = - LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, localAlias} +toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> Maybe Bool -> LocalProfile +toLocalProfile profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge, contactDomain} localAlias now badgeVerified contactDomainVerified = + LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, localAlias, contactDomain, contactDomainVerified} where - localBadge = (\b@(BadgeProof _ _ _ info) -> PeerBadge b (mkBadgeStatus now verified info)) <$> badge + localBadge = (\b@(BadgeProof _ _ _ info) -> PeerBadge b (mkBadgeStatus now badgeVerified info)) <$> badge fromLocalProfile :: LocalProfile -> Profile -fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge} = - Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge} +fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, contactDomain} = + -- the name proof is re-signed on each send + Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge, contactDomain = (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain} where - -- any stored peer proof rides the wire (receivers verify independently); the own credential is presented fresh, and a display-only badge never sends wireBadge :: LocalBadge -> Maybe BadgeProof wireBadge = \case - PeerBadge b _ -> Just b - OwnBadge _ _ -> Nothing - ShownBadge _ _ -> Nothing + PeerBadge b _ -> Just b -- stored peer proof sent as is + OwnBadge _ _ -> Nothing -- the own credential is not sent, proof is generated on send + ShownBadge _ _ -> Nothing -- a display-only badge is not sent profileBadgeVerified :: Map Int BBSPublicKey -> LocalProfile -> Profile -> IO (Maybe Bool) profileBadgeVerified keys LocalProfile {localBadge} Profile {badge = newBadge} = @@ -842,7 +846,7 @@ instance ToField GroupType where toField = toField . textEncode data PublicGroupAccess = PublicGroupAccess { groupWebPage :: Maybe Text, - groupDomain :: Maybe Text, + groupDomainClaim :: Maybe SimplexDomainClaim, domainWebPage :: Bool, allowEmbedding :: Bool } @@ -1782,6 +1786,46 @@ type ConnReqInvitation = ConnectionRequestUri 'CMInvitation type ConnReqContact = ConnectionRequestUri 'CMContact +data ConnectTarget (m :: ConnectionMode) where + CTFullContact :: ConnectionRequestUri 'CMContact -> ConnectTarget 'CMContact + CTShortContact :: ContactNameOrLink -> ConnectTarget 'CMContact + CTInv :: ConnectionLink 'CMInvitation -> ConnectTarget 'CMInvitation + +data ContactNameOrLink = CTName SimplexNameInfo | CTLink (ConnShortLink 'CMContact) + deriving (Eq, Show) + +deriving instance Eq (ConnectTarget m) + +deriving instance Show (ConnectTarget m) + +data AConnectTarget = forall m. ConnectionModeI m => ACTarget (SConnectionMode m) (ConnectTarget m) + deriving (ToJSON, FromJSON) via (StrJSON "AConnectTarget" AConnectTarget) + +instance Eq AConnectTarget where + ACTarget m t == ACTarget m' t' = case testEquality m m' of + Just Refl -> t == t' + _ -> False + +deriving instance Show AConnectTarget + +instance StrEncoding AConnectTarget where + strEncode (ACTarget _ t) = case t of + CTFullContact cr -> strEncode cr + CTShortContact (CTName n) -> strEncode n + CTShortContact (CTLink sl) -> strEncode sl + CTInv l -> strEncode l + strP = + (ACTarget SCMContact . CTShortContact . CTName <$> (lookAhead nameStart *> strP)) + <|> (aConnectTarget <$> strP) + where + nameStart = "@" <|> "#" <|> "simplex:/name" + +aConnectTarget :: AConnectionLink -> AConnectTarget +aConnectTarget (ACL SCMInvitation cl) = ACTarget SCMInvitation (CTInv cl) +aConnectTarget (ACL SCMContact cl) = ACTarget SCMContact $ case cl of + CLFull cr -> CTFullContact cr + CLShort sl -> CTShortContact (CTLink sl) + type CreatedLinkInvitation = CreatedConnLink 'CMInvitation type CreatedLinkContact = CreatedConnLink 'CMContact diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index cd7a5daea9..763cd1a963 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -52,6 +52,7 @@ import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange) import Simplex.Chat.Remote.Types import Simplex.Chat.Store (AddressSettings (..), AutoAccept (..), StoreError (..), UserContactLink (..)) import Simplex.Chat.Styled +import Simplex.Chat.Names (SimplexDomainClaim (..), claimDomain) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared @@ -147,6 +148,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRContactRatchetSyncStarted {} -> ["connection synchronization started"] CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"] CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] + CRContactDomainVerified u (Contact {profile = LocalProfile {contactDomain}}) result -> ttyUser u $ viewDomainVerified NTContact (claimDomain <$> contactDomain) result + CRGroupDomainVerified u g result -> ttyUser u $ viewDomainVerified NTPublicGroup (groupSimplexDomain g) result CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView @@ -1125,6 +1128,31 @@ simplexChatContact' = \case CLFull (CRContactUri crData) -> CLFull $ CRContactUri crData {crScheme = simplexChat} l@(CLShort _) -> l +groupSimplexDomain :: GroupInfo -> Maybe SimplexDomain +groupSimplexDomain GroupInfo {groupProfile = GroupProfile {publicGroup}} = + claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim) + +viewDomainVerified :: SimplexNameType -> Maybe SimplexDomain -> Maybe Text -> [StyledString] +viewDomainVerified nameType domain_ result = + let nameStr = maybe "name" (\d -> "SimpleX name " <> shortNameInfoStr (SimplexNameInfo nameType d)) domain_ + in case result of + Nothing -> [plain nameStr <> " verified"] + Just reason -> [plain nameStr <> " not verified: " <> plain reason] + +-- §4.7: show a peer's claimed name only with its verification context — "verified" / "verification +-- failed" when a status is recorded, "unverified" when there is a proof but no status yet, and nothing +-- at all when there is neither (an unproven, unverifiable claim is not shown). +simplexDomainLine :: SimplexNameType -> Maybe SimplexDomainClaim -> Maybe Bool -> [StyledString] +simplexDomainLine _ Nothing _ = [] +simplexDomainLine nameType (Just SimplexDomainClaim {domain, proof}) status = case status of + Just True -> [line "verified"] + Just False -> [line "verification failed"] + Nothing + | isJust proof -> [line "unverified"] + | otherwise -> [] + where + line s = plain $ "SimpleX name: " <> shortNameInfoStr (SimplexNameInfo nameType (unStrJSON domain)) <> " (" <> s <> ")" + -- TODO [short links] show all settings viewAddressSettings :: AddressSettings -> [StyledString] viewAddressSettings AddressSettings {businessAddress, autoAccept, autoReply} = case autoAccept of @@ -1142,12 +1170,14 @@ groupLink_ :: StyledString -> GroupInfo -> GroupLink -> [StyledString] groupLink_ intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMemberRole} = [ intro, "", - plain $ maybe cReqStr strEncode shortLink, - "", - "Anybody can connect to you and join group as " <> showRole acceptMemberRole <> " with: " <> highlight' "/c ", - "to show it again: " <> highlight ("/show link #" <> viewGroupName g), - "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" + plain $ maybe cReqStr strEncode shortLink ] + <> [plain ("SimpleX name: " <> shortNameInfoStr (SimplexNameInfo NTPublicGroup d)) | Just d <- [groupSimplexDomain g]] + <> [ "", + "Anybody can connect to you and join group as " <> showRole acceptMemberRole <> " with: " <> highlight' "/c ", + "to show it again: " <> highlight ("/show link #" <> viewGroupName g), + "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" + ] <> ["The group link for old clients: " <> plain cReqStr | isJust shortLink] where cReqStr = strEncode $ simplexChatContact cReq @@ -1223,6 +1253,7 @@ viewGroupLinkRelaysUpdated g groupLink relays = [ "group link:", plain $ maybe cReqStr strEncode shortLink ] + <> [plain ("SimpleX name: " <> shortNameInfoStr (SimplexNameInfo NTPublicGroup d)) | Just d <- [groupSimplexDomain g]] where GroupLink {connLinkContact = CCLink cReq shortLink} = groupLink cReqStr = strEncode $ simplexChatContact cReq @@ -1778,11 +1809,12 @@ viewContactBadge = maybe [] $ \lb -> in [plain (textEncode badgeType <> " badge - " <> st), plain expiry] viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge}, activeConn, uiThemes, customData} stats incognitoProfile = +viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge, contactDomain, contactDomainVerified}, activeConn, uiThemes, customData} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> viewContactBadge localBadge <> maybe [] viewConnectionStats stats - <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink + <> maybe [] (\l -> ["contact address: " <> plain (strEncode (simplexChatContact' l))]) contactLink + <> simplexDomainLine NTContact contactDomain contactDomainVerified <> maybe ["you've shared main profile with this contact"] (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) @@ -2017,9 +2049,9 @@ viewGroupUpdated access = pg >>= publicGroupAccess access' = pg' >>= publicGroupAccess viewAccess Nothing = " removed" - viewAccess (Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding}) = + viewAccess (Just PublicGroupAccess {groupWebPage, groupDomainClaim, domainWebPage, allowEmbedding}) = maybe "" (\u -> " web=" <> plain u) groupWebPage - <> maybe "" (\d -> " domain=" <> plain d) groupDomain + <> maybe "" (\ni -> " domain=" <> plain (strEncode ni)) (claimDomain <$> groupDomainClaim) <> (if domainWebPage then " domain_page=on" else "") <> (if allowEmbedding then " embed=on" else "") @@ -2120,12 +2152,12 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case ILPConnecting Nothing -> [invLink "connecting"] ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)] ILPKnown ct - | nextConnectPrepared ct -> [invLink ("known prepared contact " <> ttyContact' ct)] - | contactDeleted ct -> [invLink ("known deleted contact " <> ttyContact' ct)] + | nextConnectPrepared ct -> [invLink ("known prepared contact " <> ttyContact' ct)] <> contactDomainLine ct + | contactDeleted ct -> [invLink ("known deleted contact " <> ttyContact' ct)] <> contactDomainLine ct | otherwise -> - [ invLink ("known contact " <> ttyContact' ct), - "use " <> ttyToContact' ct <> highlight' "" <> " to send messages" - ] + [invLink ("known contact " <> ttyContact' ct)] + <> contactDomainLine ct + <> ["use " <> ttyToContact' ct <> highlight' "" <> " to send messages"] where invLink = ("invitation link: " <>) invOrBiz = \case @@ -2133,17 +2165,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("invitation link: " <>) CPContactAddress cap -> case cap of - CAPOk contactSLinkData ov -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] + CAPOk contactSLinkData ov _ -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] CAPOwnLink -> [ctAddr "own address"] CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"] CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] CAPKnown ct - | nextConnectPrepared ct -> [ctAddr ("known prepared contact " <> ttyContact' ct)] + | nextConnectPrepared ct -> [ctAddr ("known prepared contact " <> ttyContact' ct)] <> contactDomainLine ct | otherwise -> - [ ctAddr ("known contact " <> ttyContact' ct), - "use " <> ttyToContact' ct <> highlight' "" <> " to send messages" - ] - CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)] + [ctAddr ("known contact " <> ttyContact' ct)] + <> contactDomainLine ct + <> ["use " <> ttyToContact' ct <> highlight' "" <> " to send messages"] + CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)] <> contactDomainLine ct where ctAddr = ("contact address: " <>) addrOrBiz = \case @@ -2151,7 +2183,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("contact address: " <>) CPGroupLink glp -> case glp of - GLPOk groupSLinkInfo_ groupSLinkData ov -> + GLPOk groupSLinkInfo_ groupSLinkData ov _ -> let direct = maybe True (\(GroupShortLinkInfo {direct = d}) -> d) groupSLinkInfo_ in [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"] <> viewSigVerification ov @@ -2164,17 +2196,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case Just PreparedGroup {connLinkStartedConnection} -> case memberStatus m of GSMemUnknown | connLinkStartedConnection -> connecting g - | otherwise -> [knownGroup "prepared "] + | otherwise -> [knownGroup "prepared "] <> groupDomainLine g GSMemAccepted -> connecting g _ - | memberRemoved m -> [knownGroup "deleted "] -- it should not get here, as this plan is returned as GLPOk + | memberRemoved m -> [knownGroup "deleted "] <> groupDomainLine g -- it should not get here, as this plan is returned as GLPOk | otherwise -> knownActive _ -> knownActive where knownActive = - [ knownGroup "", - "use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages" - ] + [knownGroup ""] + <> groupDomainLine g + <> ["use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages"] knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g GLPNoRelays _ -> [grpLink "channel has no active relays, please try to join later"] GLPUpdateRequired _ -> [grpLink "this group requires a newer version of the app, please upgrade"] @@ -2192,6 +2224,13 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case nextConnectPrepared Contact {preparedContact, activeConn} = case preparedContact of Just _ -> maybe True (\c -> connStatus c == ConnPrepared) activeConn _ -> False + contactDomainLine :: Contact -> [StyledString] + contactDomainLine Contact {profile = LocalProfile {contactDomain, contactDomainVerified}} = + simplexDomainLine NTContact contactDomain contactDomainVerified + groupDomainLine :: GroupInfo -> [StyledString] + groupDomainLine GroupInfo {groupDomainVerified, groupProfile = GroupProfile {publicGroup}} = do + let domain = publicGroup >>= publicGroupAccess >>= groupDomainClaim + in simplexDomainLine NTPublicGroup domain groupDomainVerified viewSigVerification = \case Just OVVerified -> ["owner signature: verified"] Just (OVFailed r) -> ["owner signature: FAILED (" <> plain r <> ")"] @@ -2643,6 +2682,11 @@ viewChatError isCmd logLevel testView = \case CEChatNotStopped -> ["error: chat not stopped"] CEChatStoreChanged -> ["error: chat store changed, please restart chat"] CEInvalidConnReq -> viewInvalidConnReq + CESimplexDomainNotReady domain domainErr -> + let reason = case domainErr of + SDENoValidLink -> "has no valid connection link" + SDEUnknownDomain -> "is not included in the connection link's profile" + in [plain $ "SimpleX name " <> strEncode domain <> " " <> reason] CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr] CEInvalidChatMessage Connection {connId} msgMeta_ msg e -> [ plain $ diff --git a/tests/Bots/BroadcastTests.hs b/tests/Bots/BroadcastTests.hs index 051ee6b304..bfe7b96c7d 100644 --- a/tests/Bots/BroadcastTests.hs +++ b/tests/Bots/BroadcastTests.hs @@ -33,7 +33,7 @@ withBroadcastBot opts test = bot = simplexChatCore testCfg (mkChatOpts opts) $ broadcastBot opts broadcastBotProfile :: Profile -broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing} +broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing} mkBotOpts :: TestParams -> [KnownContact] -> BroadcastBotOpts mkBotOpts ps publishers = diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 438a3f9802..1c6da2e1ed 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -101,7 +101,7 @@ directoryServiceTests = do it "should update subscriber count periodically" testLinkCheckUpdatesCount directoryProfile :: Profile -directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing} +directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing} mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> Maybe FilePath -> DirectoryOpts mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index c955ca1353..c803570d99 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -57,9 +57,9 @@ import Simplex.Messaging.Client (ProtocolClientConfig (..)) import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Protocol (sndAuthKeySMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM (ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) +import NameResolver (NameRegistry, resolverNamesConfig, withNameResolver) import Simplex.Messaging.Server.MsgStore.STM (STMMsgStore) import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Server (ServerCredentials (..), mkTransportServerConfig) @@ -202,13 +202,6 @@ testAgentCfg = where RetryInterval2 {riFast, riSlow} = messageRetryInterval aCfg -testAgentCfgNoShortLinks :: AgentConfig -testAgentCfgNoShortLinks = - testAgentCfg - { smpClientVRange = mkVersionRange (Version 1) sndAuthKeySMPClientVersion, -- v3 - smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange minClientSMPRelayVersion (Version 14)} -- before shortLinksSMPVersion - } - testCfg :: ChatConfig testCfg = defaultChatConfig @@ -221,9 +214,6 @@ testCfg = confirmMigrations = MCYesUp } -testCfgNoShortLinks :: ChatConfig -testCfgNoShortLinks = testCfg {agentConfig = testAgentCfgNoShortLinks} - testAgentCfgVPrev :: AgentConfig testAgentCfgVPrev = testAgentCfg @@ -586,6 +576,8 @@ smpServerCfg = smpAgentCfg = defaultSMPClientAgentConfig, allowSMPProxy = True, serverClientConcurrency = 16, + serverResolverConcurrency = 1000, + namesConfig = Nothing, information = Nothing, startOptions = StartOptions {maintenance = False, compactLog = False, logLevel = LogError, skipWarnings = False, confirmMigrations = MCYesUp} } @@ -599,6 +591,13 @@ withSmpServer = withSmpServer' smpServerCfg withSmpServer' :: ServerConfig STMMsgStore -> IO a -> IO a withSmpServer' cfg = serverBracket (\started -> runSMPServerBlocking started cfg Nothing) +-- | SMP server with a local names resolver attached; the action gets the resolver +-- registry to map names to the addresses it creates. +withSmpServerAndNames :: (NameRegistry -> IO a) -> IO a +withSmpServerAndNames action = + withNameResolver $ \port reg -> + withSmpServer' smpServerCfg {namesConfig = Just (resolverNamesConfig port)} (action reg) + xftpTestPort :: ServiceName xftpTestPort = "7002" diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 7acfae1a95..cdc78e2ec8 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1210,20 +1210,20 @@ testOperators = alice <##. "Current conditions: 2." alice ##> "/_operators" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required" - alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: required" + alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: required" alice <##. "The new conditions will be accepted for SimpleX Chat Ltd, InFlux Technologies Limited at " -- set conditions notified alice ##> "/_conditions_notified 2" alice <## "ok" alice ##> "/_operators" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required" - alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: required" + alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: required" alice ##> "/_conditions" alice <##. "Current conditions: 2 (notified)." -- accept conditions alice ##> "/_accept_conditions 2 1,2" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" - alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: accepted (" + alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: accepted (" -- update operators alice ##> "/operators 2:on:smp=proxy:xftp=off" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" diff --git a/tests/ChatTests/Names.hs b/tests/ChatTests/Names.hs new file mode 100644 index 0000000000..7c1ef40155 --- /dev/null +++ b/tests/ChatTests/Names.hs @@ -0,0 +1,143 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PostfixOperators #-} + +module ChatTests.Names where + +import ChatClient +import ChatTests.DBUtils +import ChatTests.Groups (prepareChannel1Relay) +import ChatTests.Utils +import Control.Concurrent.Async (concurrently_) +import qualified Data.Text as T +import NameResolver +import Simplex.Messaging.SimplexName (SimplexDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..)) +import Test.Hspec hiding (it) + +chatNamesTests :: SpecWith TestParams +chatNamesTests = do + it "connect by resolved name" testConnectByName + it "connect by name not claimed in link profile is rejected" testConnectByNameNotClaimed + it "connect by name to a known contact not claimed in profile is rejected" testConnectByNameKnownContactNotClaimed + it "connect by unregistered name fails to resolve" testConnectByNameNotFound + it "set name not resolving to own address is rejected" testSetNameNotOwnAddress + it "connect by channel name" testConnectByChannelName + +testConnectByName :: HasCallStack => TestParams -> IO () +testConnectByName ps = withSmpServerAndNames $ \reg -> + testChat2 aliceProfile bobProfile (test reg) ps + where + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) + test reg alice bob = do + alice ##> "/ad" + (shortLink, _) <- getContactLinks alice True + registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink)) + alice ##> "/_set domain 1 alice.simplex" + alice <## "new contact address set" + bob ##> "/c @alice.simplex" + bob <## "alice: connection started" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + bob ##> "/i alice" + bob <## "contact ID: 2" + bob <## "receiving messages via: localhost" + bob <## "sending messages via: localhost" + _ <- getTermLine bob + bob <## "SimpleX name: @alice.simplex (verified)" + bob <## "you've shared main profile with this contact" + bob <## "connection not verified, use /code command to see security code" + bob <## "quantum resistant end-to-end encryption" + _ <- getTermLine bob + pure () + +testConnectByNameNotClaimed :: HasCallStack => TestParams -> IO () +testConnectByNameNotClaimed ps = withSmpServerAndNames $ \reg -> + testChat2 aliceProfile bobProfile (test reg) ps + where + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) + test reg alice bob = do + alice ##> "/ad" + (shortLink, _) <- getContactLinks alice True + registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink)) + bob ##> "/c @alice.simplex" + bob <## "SimpleX name alice.simplex is not included in the connection link's profile" + +testConnectByNameKnownContactNotClaimed :: HasCallStack => TestParams -> IO () +testConnectByNameKnownContactNotClaimed ps = withSmpServerAndNames $ \reg -> + testChat2 aliceProfile bobProfile (test reg) ps + where + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) + test reg alice bob = do + alice ##> "/ad" + (shortLink, _) <- getContactLinks alice True + bob ##> ("/c " <> shortLink) + bob <## "connection request sent!" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink)) + bob ##> "/c @alice.simplex" + bob <## "SimpleX name alice.simplex is not included in the connection link's profile" + +testConnectByNameNotFound :: HasCallStack => TestParams -> IO () +testConnectByNameNotFound ps = withSmpServerAndNames $ \_reg -> + testChat2 aliceProfile bobProfile test ps + where + test _alice bob = do + bob ##> "/c @nobody.simplex" + bob .<## "smpErr = NAME {nameErr = NOT_FOUND}}" + +testSetNameNotOwnAddress :: HasCallStack => TestParams -> IO () +testSetNameNotOwnAddress ps = withSmpServerAndNames $ \reg -> + testChat2 aliceProfile bobProfile (test reg) ps + where + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) + test reg alice bob = do + bob ##> "/ad" + (bobShortLink, _) <- getContactLinks bob True + registerName reg aliceName (contactNameRecord "alice" (T.pack bobShortLink)) + alice ##> "/ad" + _ <- getContactLinks alice True + alice ##> "/_set domain 1 alice.simplex" + alice <## "SimpleX name alice.simplex has no valid connection link" + +testConnectByChannelName :: HasCallStack => TestParams -> IO () +testConnectByChannelName ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + (shortLink, _) <- prepareChannel1Relay "team" alice cath + registerName reg teamName (channelNameRecord "team" (T.pack shortLink)) + alice ##> "/public group access #team domain=team.simplex" + alice <## "updated public group access: domain=team.simplex" + cath <## "alice updated group #team: (signed)" + cath <## "updated public group access: domain=team.simplex" + bob ##> "/c #team.simplex" + bob <## "#team: connection started" + concurrentlyN_ + [ bob + <### [ "#team: joining the group (connecting to relay cath)...", + "#team: you joined the group (connected to relay cath)" + ] + , do + cath <## "bob (Bob): accepting request to join group #team..." + cath <## "#team: bob joined the group" + , alice <### [EndsWith "introduced bob (Bob) in the channel"] + ] + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: known group #team" + bob <## "SimpleX name: #team (verified)" + bob <## "use #team to send messages" + where + teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" []) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index fb56d39aae..581aaec879 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -65,12 +65,9 @@ chatProfileTests = do it "reject contact and delete contact link" testRejectContactAndDeleteUserContact it "keep connection requests when contact link deleted" testKeepConnectionRequests it "connected contact works when contact link deleted" testContactLinkDeletedConnectedContactWorks - -- TODO [short links] test auto-reply with current version, with connecting client not preparing contact it "auto-reply message" testAutoReplyMessage - it "auto-reply message in incognito" testAutoReplyMessageInIncognito describe "business address" $ do it "create and connect via business address" testBusinessAddress - -- TODO [short links] test business auto-reply with current version, with connecting client not preparing contact it "update profiles with business address" testBusinessUpdateProfiles describe "contact address connection plan" $ do it "contact address ok to connect; known contact" testPlanAddressOkKnown @@ -82,7 +79,6 @@ chatProfileTests = do describe "incognito" $ do it "connect incognito via invitation link" testConnectIncognitoInvitationLink it "connect incognito via contact address" testConnectIncognitoContactAddress - it "accept contact request incognito" testAcceptContactRequestIncognito it "set connection incognito" testSetConnectionIncognito it "reset connection incognito" testResetConnectionIncognito it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation @@ -501,7 +497,7 @@ testMultiWordProfileNames = aliceProfile' = baseProfile {displayName = "Alice Jones"} bobProfile' = baseProfile {displayName = "Bob James"} cathProfile' = baseProfile {displayName = "Cath Johnson"} - baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing} + baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing} testUserContactLink :: HasCallStack => TestParams -> IO () testUserContactLink = @@ -974,10 +970,10 @@ testContactLinkDeletedConnectedContactWorks = testChat2 aliceProfile bobProfile bob @@@ [("@alice", "hey")] testAutoReplyMessage :: HasCallStack => TestParams -> IO () -testAutoReplyMessage = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile $ +testAutoReplyMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/ad" - cLink <- getContactLinkNoShortLink alice True + cLink <- getContactLink alice True alice ##> "/auto_accept on incognito=off text hello!" alice <## "auto_accept on" alice <## "auto reply:" @@ -995,31 +991,6 @@ testAutoReplyMessage = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile alice <## "bob (Bob): contact is connected" ] -testAutoReplyMessageInIncognito :: HasCallStack => TestParams -> IO () -testAutoReplyMessageInIncognito = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile $ - \alice bob -> do - alice ##> "/ad" - cLink <- getContactLinkNoShortLink alice True - alice ##> "/auto_accept on incognito=on text hello!" - alice <## "auto_accept on, incognito" - alice <## "auto reply:" - alice <## "hello!" - - bob ##> ("/c " <> cLink) - bob <## "connection request sent!" - alice <## "bob (Bob): accepting contact request..." - alice <## "bob (Bob): you can send messages to contact" - alice <# "i @bob hello!" - aliceIncognito <- getTermLine alice - concurrentlyN_ - [ do - bob <# (aliceIncognito <> "> hello!") - bob <## (aliceIncognito <> ": contact is connected"), - do - alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) - alice <## "use /i bob to print out this incognito profile again" - ] - testBusinessAddress :: HasCallStack => TestParams -> IO () testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile $ \biz alice bob -> do @@ -1075,10 +1046,10 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice (biz <# "#bob bob_1> hey there") testBusinessUpdateProfiles :: HasCallStack => TestParams -> IO () -testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile aliceProfile bobProfile cathProfile $ +testBusinessUpdateProfiles = testChat4 businessProfile aliceProfile bobProfile cathProfile $ \biz alice bob cath -> do biz ##> "/ad" - cLink <- getContactLinkNoShortLink biz True + cLink <- getContactLink biz True biz ##> "/auto_accept on business text Welcome" biz <## "auto_accept on, business" biz <## "auto reply:" @@ -1108,7 +1079,7 @@ testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile al biz ##> "/mr alisa alisa_1 admin" biz <## "#alisa: you changed the role of alisa_1 to admin" alice <## "#biz: biz_1 changed your role from member to admin" - connectUsersNoShortLink alice bob + connectUsers alice bob alice ##> "/a #biz bob" alice <## "invitation to join the group #biz sent to bob" bob <## "#biz (Biz Inc): alisa invites you to join the group as member" @@ -1139,7 +1110,7 @@ testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile al alice <# "#biz robert> hi there" biz <# "#alisa robert> hi there" -- add business team member - connectUsersNoShortLink biz cath + connectUsers biz cath biz ##> "/a #alisa cath" biz <## "invitation to join the group #alisa sent to cath" cath <## "#alisa: biz invites you to join the group as member" @@ -1628,54 +1599,6 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ (bob TestParams -> IO () -testAcceptContactRequestIncognito = testChatCfg3 testCfgNoShortLinks aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - alice ##> "/ad" - cLink <- getContactLinkNoShortLink alice True - -- GUI /_accept api - bob ##> ("/c " <> cLink) - alice <#? bob - alice ##> "/_accept incognito=on 1" - alice <## "bob (Bob): accepting contact request, you can send messages to contact" - aliceIncognitoBob <- getTermLine alice - concurrentlyN_ - [ bob <## (aliceIncognitoBob <> ": contact is connected"), - do - alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognitoBob) - alice <## "use /i bob to print out this incognito profile again" - ] - -- conversation is incognito - alice ?#> "@bob my profile is totally inconspicuous" - bob <# (aliceIncognitoBob <> "> my profile is totally inconspicuous") - bob #> ("@" <> aliceIncognitoBob <> " I know!") - alice ?<# "bob> I know!" - -- list contacts - alice ##> "/contacts" - alice <## "i bob (Bob)" - alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognitoBob] - -- delete contact, incognito profile is deleted - alice ##> "/d bob" - alice <## "bob: contact is deleted" - bob <## (aliceIncognitoBob <> " deleted contact with you") - alice ##> "/contacts" - (alice ("/c " <> cLink) - alice <#? cath - alice ##> "/accept incognito cath" - alice <## "cath (Catherine): accepting contact request, you can send messages to contact" - aliceIncognitoCath <- getTermLine alice - concurrentlyN_ - [ cath <## (aliceIncognitoCath <> ": contact is connected"), - do - alice <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> aliceIncognitoCath) - alice <## "use /i cath to print out this incognito profile again" - ] - alice `hasContactProfiles` ["alice", "cath", T.pack aliceIncognitoCath] - cath `hasContactProfiles` ["cath", T.pack aliceIncognitoCath] - testSetConnectionIncognito :: HasCallStack => TestParams -> IO () testSetConnectionIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index e0e694f0cb..a613a13df6 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -88,7 +88,7 @@ serviceProfile :: Profile serviceProfile = mkProfile "service_user" "Service user" Nothing mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile -mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing} +mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing} it :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation)) it name test = diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index 2a5328ff26..e315b59f5e 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -10,7 +10,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Markdown -import Simplex.Messaging.Agent.Protocol (SimplexNameDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..)) +import Simplex.Messaging.Agent.Protocol (SimplexDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util ((<$$>)) import System.Console.ANSI.Types @@ -400,7 +400,7 @@ command' :: Text -> Text -> FormattedText command' = FormattedText . Just . Command sname :: SimplexNameType -> SimplexTLD -> Text -> [Text] -> Text -> Markdown -sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt (SimplexNameDomain ns dom sub)) (pfx <> txt) +sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt (SimplexDomain ns dom sub)) (pfx <> txt) where pfx = case nt of NTPublicGroup -> "#"; NTContact -> "@" diff --git a/tests/NameResolver.hs b/tests/NameResolver.hs new file mode 100644 index 0000000000..b7c0aaf2fc --- /dev/null +++ b/tests/NameResolver.hs @@ -0,0 +1,83 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Local HTTP names resolver for chat tests, copied from simplexmq's +-- NamesResolverServer and made dynamic: it answers /resolve/ from a +-- mutable name -> NameRecord registry, so a test can resolve a name to the +-- address it just created. +module NameResolver + ( NameRegistry, + withNameResolver, + registerName, + contactNameRecord, + channelNameRecord, + resolverNamesConfig, + ) +where + +import Control.Concurrent.STM +import qualified Data.Aeson as J +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Text (Text) +import Network.HTTP.Types (hContentType, notFound404, ok200) +import Network.Wai (Application, pathInfo, responseLBS) +import qualified Network.Wai.Handler.Warp as Warp +import Simplex.Messaging.Names.Record (NameRecord (..)) +import Simplex.Messaging.Server.Names (NamesConfig (..)) +import Simplex.Messaging.SimplexName (SimplexNameInfo (..), fullDomainName) + +type NameRegistry = TVar (Map Text NameRecord) + +-- | Run an action with a local resolver on a free port and its registry (keyed +-- by full domain name, what the resolver looks the name up by). +withNameResolver :: (Int -> TVar (Map Text NameRecord) -> IO a) -> IO a +withNameResolver action = do + reg <- newTVarIO M.empty + Warp.withApplication (pure (app reg)) $ \port -> action port reg + where + app :: TVar (Map Text NameRecord) -> Application + app reg req send = do + (st, body) <- case pathInfo req of + ["health"] -> pure (ok200, "{}") + ["resolve", d] -> maybe (notFound404, "{}") (\r -> (ok200, J.encode r)) . M.lookup d <$> readTVarIO reg + _ -> pure (notFound404, "{}") + send $ responseLBS st [(hContentType, "application/json")] body + +-- | Register a name's domain to resolve to the given record. +registerName :: TVar (Map Text NameRecord) -> SimplexNameInfo -> NameRecord -> IO () +registerName reg SimplexNameInfo {nameDomain} r = + atomically $ modifyTVar' reg $ M.insert (fullDomainName nameDomain) r + +contactNameRecord :: Text -> Text -> NameRecord +contactNameRecord name link = (emptyRecord name) {nrSimplexContact = [link]} + +channelNameRecord :: Text -> Text -> NameRecord +channelNameRecord name link = (emptyRecord name) {nrSimplexChannel = [link]} + +emptyRecord :: Text -> NameRecord +emptyRecord name = + NameRecord + { nrName = name, + nrNickname = "", + nrWebsite = "", + nrLocation = "", + nrSimplexContact = [], + nrSimplexChannel = [], + nrEth = Nothing, + nrBtc = Nothing, + nrXmr = Nothing, + nrDot = Nothing, + nrOwner = "", + nrResolver = "" + } + +-- | NamesConfig for a chat test SMP server pointing at this resolver. +resolverNamesConfig :: Int -> NamesConfig +resolverNamesConfig port = + NamesConfig + { resolverEndpoint = "http://127.0.0.1:" <> show port, + resolverAuth = Nothing, + resolverTimeoutMs = 1000, + resolverMaxResponseBytes = 65536 + } diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs index 5e659dd82c..d0b48a9019 100644 --- a/tests/OperatorTests.hs +++ b/tests/OperatorTests.hs @@ -38,9 +38,9 @@ validateServersTest :: Spec validateServersTest = describe "validate user servers" $ do it "should pass valid user servers" $ validateUserServers [valid] [] `shouldBe` ([], []) it "should fail without servers" $ do - validateUserServers [invalidNoServers] [] `shouldBe` ([USENoServers aSMP Nothing], []) - validateUserServers [invalidDisabled] [] `shouldBe` ([USENoServers aSMP Nothing], []) - validateUserServers [invalidDisabledOp] [] `shouldBe` ([USENoServers aSMP Nothing, USENoServers aXFTP Nothing], [USWNoChatRelays Nothing]) + validateUserServers [invalidNoServers] [] `shouldBe` ([USENoServers aSMP Nothing], [USWNoNamesServers Nothing]) + validateUserServers [invalidDisabled] [] `shouldBe` ([USENoServers aSMP Nothing], [USWNoNamesServers Nothing]) + validateUserServers [invalidDisabledOp] [] `shouldBe` ([USENoServers aSMP Nothing, USENoServers aXFTP Nothing], [USWNoChatRelays Nothing, USWNoNamesServers Nothing]) it "should fail without servers with storage role" $ do validateUserServers [invalidNoStorage] [] `shouldBe` ([USEStorageMissing aSMP Nothing], []) it "should fail with duplicate host" $ do diff --git a/tests/PostgresSchemaDump.hs b/tests/PostgresSchemaDump.hs index 0cd79ac513..b36b7faeaf 100644 --- a/tests/PostgresSchemaDump.hs +++ b/tests/PostgresSchemaDump.hs @@ -80,5 +80,7 @@ skipComparisonForDownMigrations = -- group_member_intro_id field moves "20251128_migrate_member_relations", -- on down migration single_sender_group_member_id column is re-added at the end of the table - "20260529_delivery_job_senders" + "20260529_delivery_job_senders", + -- group_domain is removed + "20260603_simplex_name" ] diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 2592420d01..3cfb367382 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -3,6 +3,7 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ProtocolTests where @@ -107,7 +108,7 @@ testGroupPreferences :: Maybe GroupPreferences testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, commands = Nothing} testProfile :: Profile -testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing} +testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} testGroupProfile :: GroupProfile testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} @@ -240,7 +241,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XInfo testProfile it "x.info with empty full name" $ "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" - #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing} + #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} it "x.contact with xContactId" $ "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile (Just $ XContactId "\1\2\3\4") Nothing Nothing diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 2336fd56dd..783f6587bf 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -145,7 +145,9 @@ skipComparisonForDownMigrations = -- on down migration single_sender_group_member_id column and its index -- are re-added at the end of the table / file (ALTER TABLE ADD COLUMN -- appends; CREATE INDEX appends). - "20260529_delivery_job_senders" + "20260529_delivery_job_senders", + -- group_domain is removed + "20260603_simplex_name" ] getSchema :: FilePath -> FilePath -> IO String diff --git a/tests/Test.hs b/tests/Test.hs index 874428bc1f..515098c5d1 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -8,6 +8,7 @@ import Bots.DirectoryTests import ChatClient import ChatTests import ChatTests.DBUtils +import ChatTests.Names (chatNamesTests) import ChatTests.Utils (xdescribe'') import Control.Logger.Simple import Data.Time.Clock.System @@ -71,6 +72,9 @@ main = do describe "Message batching" batchingTests describe "Operators" operatorTests describe "Random servers" randomServersTests +#if !defined(dbPostgres) + around (tmpTestBracket chatQueryStats agentQueryStats) $ describe "names tests" chatNamesTests +#endif #if defined(dbPostgres) createdDropDb . around testBracket #else @@ -96,6 +100,8 @@ main = do #else testBracket chatQueryStats agentQueryStats test = withSmpServer $ tmpBracket $ \tmpPath -> test TestParams {tmpPath, chatQueryStats, agentQueryStats, printOutput = False} + tmpTestBracket chatQueryStats agentQueryStats test = + tmpBracket $ \tmpPath -> test TestParams {tmpPath, chatQueryStats, agentQueryStats, printOutput = False} #endif tmpBracket test = do t <- getSystemTime From e3502c57388a48339676733018200e268f591abe Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 3 Jul 2026 13:47:45 +0100 Subject: [PATCH 15/21] ios: reliably show API alerts (use UIKit) (#7195) * ios: reliably show API alerts (use UIKit) * fix infinite recursion --- apps/ios/Shared/Model/SimpleXAPI.swift | 286 +++++++++--------- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 27 +- .../Chat/ChatItem/CIRcvDecryptionError.swift | 6 +- .../Chat/Group/AddGroupMembersView.swift | 5 +- .../Views/Chat/Group/GroupLinkView.swift | 11 +- .../Chat/Group/GroupMemberInfoView.swift | 22 +- .../Views/ChatList/ChatListNavLink.swift | 26 +- .../ChatList/ContactConnectionInfo.swift | 6 +- .../ios/Shared/Views/Helpers/ShareSheet.swift | 4 + .../Shared/Views/NewChat/NewChatView.swift | 42 +-- .../Onboarding/CreateSimpleXAddress.swift | 16 +- .../Shared/Views/Onboarding/YourNetwork.swift | 8 +- .../RemoteAccess/ConnectDesktopView.swift | 3 +- .../Views/UserSettings/UserAddressView.swift | 10 +- .../Views/UserSettings/UserProfilesView.swift | 10 +- apps/ios/SimpleXChat/ErrorAlert.swift | 106 +++++-- 16 files changed, 287 insertions(+), 301 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 3b610663dc..d7ee777d5b 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -569,7 +569,7 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async return cItems } if let networkErrorAlert = networkErrorAlert(r) { - AlertManager.shared.showAlert(networkErrorAlert) + await MainActor.run { showAlert(networkErrorAlert) } } else { sendMessageErrorAlert(r.unexpected) } @@ -1003,15 +1003,15 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo return nil } -func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactConnection)?, Alert?) { +func apiAddContact(incognito: Bool) async -> (CreatedConnLink, PendingContactConnection)? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiAddContact: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) - if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) } - let alert: Alert? = if let r { connectionErrorAlert(r) } else { nil } - return (nil, alert) + if case let .result(.invitation(_, connLinkInv, connection)) = r { return (connLinkInv, connection) } + if let r { await MainActor.run { showAlert(connectionErrorAlert(r)) } } + return nil } func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? { @@ -1026,130 +1026,124 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi if let r { throw r.unexpected } else { return nil } } -func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { +func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> (CreatedConnLink, ConnectionPlan)? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectPlan: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, linkOwnerSig: linkOwnerSig), inProgress: inProgress) - if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } - let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } - return (nil, alert) + if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return (connLink, connPlan) } + if let r { await apiConnectResponseAlert(r) } + return nil } func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? { - let (r, alert) = await apiConnect_(incognito: incognito, connLink: connLink) - if let alert = alert { - AlertManager.shared.showAlert(alert) - return nil - } else { - return r - } -} - -func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqType, PendingContactConnection)?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnect: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) let m = ChatModel.shared switch r { case let .result(.sentConfirmation(_, connection)): - return ((.invitation, connection), nil) + return (.invitation, connection) case let .result(.sentInvitation(_, connection)): - return ((.contact, connection), nil) + return (.contact, connection) case let .result(.contactAlreadyExists(_, contact)): if let c = m.getContactChat(contact.contactId) { ItemsModel.shared.loadOpenChat(c.id) } - let alert = contactAlreadyExistsAlert(contact) - return (nil, alert) + await contactAlreadyExistsAlert(contact) + return nil default: () } - let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } - return (nil, alert) + if let r { await apiConnectResponseAlert(r) } + return nil } -private func apiConnectResponseAlert(_ r: APIResult) -> Alert { - switch r.unexpected { - case .error(.invalidConnReq): - mkAlert( - title: "Invalid connection link", - message: "Please check that you used the correct link or ask your contact to send you another one." - ) - case .error(.unsupportedConnReq): - mkAlert( - title: "Unsupported connection link", - message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." - ) - case let .error(.simplexDomainNotReady(domain, err)): - switch err { - case .noValidLink: - mkAlert( - title: "No valid link", - message: "The SimpleX name \(domain.fullDomainName) is registered, but it has no valid link." +private func apiConnectResponseAlert(_ r: APIResult) async { + await MainActor.run { + switch r.unexpected { + case .error(.invalidConnReq): + showAlert( + NSLocalizedString("Invalid connection link", comment: ""), + message: NSLocalizedString("Please check that you used the correct link or ask your contact to send you another one.", comment: "") ) - case .unknownDomain: - mkAlert( - title: "Unconfirmed name", - message: "The SimpleX name \(domain.fullDomainName) is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." + case .error(.unsupportedConnReq): + showAlert( + NSLocalizedString("Unsupported connection link", comment: ""), + message: NSLocalizedString("This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.", comment: "") ) - } - case .errorAgent(.NO_NAME_SERVERS): - mkAlert( - title: "SimpleX name error", - message: "None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." - ) - case .errorAgent(.SMP(_, .AUTH)): - mkAlert( - title: "Connection error (AUTH)", - message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." - ) - case let .errorAgent(.SMP(_, .BLOCKED(info))): - Alert( - title: Text("Connection blocked"), - message: Text("Connection is blocked by server operator:\n\(info.reason.text)"), - primaryButton: .default(Text("Ok")), - secondaryButton: .default(Text("How it works")) { - DispatchQueue.main.async { - UIApplication.shared.open(contentModerationPostLink) - } + case let .error(.simplexDomainNotReady(domain, err)): + switch err { + case .noValidLink: + showAlert( + NSLocalizedString("No valid link", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("The SimpleX name %@ is registered, but it has no valid link.", comment: ""), domain.fullDomainName) + ) + case .unknownDomain: + showAlert( + NSLocalizedString("Unconfirmed name", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.", comment: ""), domain.fullDomainName) + ) } - ) - case .errorAgent(.SMP(_, .QUOTA)): - mkAlert( - title: "Undelivered messages", - message: "The connection reached the limit of undelivered messages, your contact may be offline." - ) - case let .errorAgent(.INTERNAL(internalErr)): - if internalErr == "SEUniqueID" { - mkAlert( - title: "Already connected?", - message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(internalErr))." + case .errorAgent(.NO_NAME_SERVERS): + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: NSLocalizedString("None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link.", comment: "") ) - } else { - connectionErrorAlert(r) + case .errorAgent(.SMP(_, .AUTH)): + showAlert( + NSLocalizedString("Connection error (AUTH)", comment: ""), + message: NSLocalizedString("Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.", comment: "") + ) + case let .errorAgent(.SMP(_, .BLOCKED(info))): + showAlert( + NSLocalizedString("Connection blocked", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Connection is blocked by server operator:\n%@", comment: ""), info.reason.text), + actions: {[ + okAlertAction, + UIAlertAction(title: NSLocalizedString("How it works", comment: ""), style: .default) { _ in + DispatchQueue.main.async { + UIApplication.shared.open(contentModerationPostLink) + } + } + ]} + ) + case .errorAgent(.SMP(_, .QUOTA)): + showAlert( + NSLocalizedString("Undelivered messages", comment: ""), + message: NSLocalizedString("The connection reached the limit of undelivered messages, your contact may be offline.", comment: "") + ) + case let .errorAgent(.INTERNAL(internalErr)): + if internalErr == "SEUniqueID" { + showAlert( + NSLocalizedString("Already connected?", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("It seems like you are already connected via this link. If it is not the case, there was an error (%@).", comment: ""), internalErr) + ) + } else { + showAlert(connectionErrorAlert(r)) + } + case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))): + switch nameErr { + case .NOT_FOUND: + showAlert( + NSLocalizedString("Name not found", comment: ""), + message: NSLocalizedString("This SimpleX name is not registered. Please check the name.", comment: "") + ) + case .NO_RESOLVER: + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server %@ does not support name resolution. Configure servers, or use a connection link.", comment: ""), serverAddress) + ) + case let .RESOLVER(resolverErr): + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Resolver error: %@", comment: ""), resolverErr) + ) + } + default: showAlert(connectionErrorAlert(r)) } - case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))): - switch nameErr { - case .NOT_FOUND: - mkAlert( - title: "Name not found", - message: "This SimpleX name is not registered. Please check the name." - ) - case .NO_RESOLVER: - mkAlert( - title: "SimpleX name error", - message: "Server \(serverAddress) does not support name resolution. Configure servers, or use a connection link." - ) - case let .RESOLVER(resolverErr): - mkAlert( - title: "SimpleX name error", - message: "Resolver error: \(resolverErr)" - ) - } - default: connectionErrorAlert(r) } } @@ -1162,36 +1156,34 @@ func connErrorText(_ e: ChatError) -> String { case .errorAgent(.SMP(_, .AUTH)): NSLocalizedString("Connection error (AUTH)", comment: "conn error description") case let .errorAgent(.SMP(_, .BLOCKED(info))): - NSLocalizedString("Connection blocked: \(info.reason.text)", comment: "conn error description") + String.localizedStringWithFormat(NSLocalizedString("Connection blocked: %@", comment: "conn error description"), info.reason.text) case .errorAgent(.SMP(_, .QUOTA)): NSLocalizedString("The connection reached the limit of undelivered messages", comment: "conn error description") default: if getNetworkErrorAlert(e) != nil { NSLocalizedString("Network error", comment: "conn error description") } else { - "\(NSLocalizedString("Error", comment: "conn error description")): \(responseError(e))" + String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "conn error description"), responseError(e)) } } } -func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { - mkAlert( - title: "Contact already exists", - message: "You are already connected to \(contact.displayName)." - ) -} - -private func connectionErrorAlert(_ r: APIResult) -> Alert { - if let networkErrorAlert = networkErrorAlert(r) { - return networkErrorAlert - } else { - return mkAlert( - title: "Connection error", - message: "Error: \(responseError(r.unexpected))" +func contactAlreadyExistsAlert(_ contact: Contact) async { + await MainActor.run { + showAlert( + NSLocalizedString("Contact already exists", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("You are already connected to %@.", comment: ""), contact.displayName) ) } } +private func connectionErrorAlert(_ r: APIResult) -> (title: String, message: String?) { + networkErrorAlert(r) ?? ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(r.unexpected)) + ) +} + func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData { let userId = try currentUserId("apiPrepareContact") let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain)) @@ -1221,30 +1213,29 @@ func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws - func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async -> Contact? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToContact(_, contact)) = r { return contact } - if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + if let r { await apiConnectResponseAlert(r) } return nil } func apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) async -> (GroupInfo, [RelayConnectionResult])? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToGroup(_, groupInfo, relayResults)) = r { return (groupInfo, relayResults) } - if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + if let r { await apiConnectResponseAlert(r) } return nil } -func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Contact?, Alert?) { +func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> Contact? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectContactViaAddress: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) - if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) } + if case let .result(.sentInvitationToContact(_, contact, _)) = r { return contact } if let r { logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") - return (nil, connectionErrorAlert(r)) - } else { - return (nil, nil) + await MainActor.run { showAlert(connectionErrorAlert(r)) } } + return nil } func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws { @@ -1365,16 +1356,16 @@ func apiSetProfileAddress(on: Bool) async throws -> User? { } } -// name is the encoded SimplexName (e.g. "@alice.simplex"); nil clears it -// owner-specific SNENoValidLink wording; everything else reuses the general apiConnectResponseAlert -func showSetSimplexNameError(_ r: APIResult, isChannel: Bool) { +func showSetSimplexNameError(_ r: APIResult, isChannel: Bool) async { if case let .error(.simplexDomainNotReady(domain, .noValidLink)) = r.unexpected { let format = isChannel ? NSLocalizedString("The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page.", comment: "alert message") : NSLocalizedString("The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.", comment: "alert message") - showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName)) + await MainActor.run { + showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName)) + } } else { - AlertManager.shared.showAlert(apiConnectResponseAlert(r)) + await apiConnectResponseAlert(r) } } @@ -1385,7 +1376,7 @@ func apiSetUserDomain(_ simplexDomain: String?) async throws -> User { case let .result(.userProfileUpdated(user, _, _, _)): return user case let .result(.userProfileNoChange(user)): return user default: - showSetSimplexNameError(r, isChannel: false) + await showSetSimplexNameError(r, isChannel: false) throw r.unexpected } } @@ -1502,23 +1493,22 @@ func apiSetUserAddressSettings(_ settings: AddressSettings) async throws -> User func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) - let am = AlertManager.shared if case let .result(.acceptingContactRequest(_, contact)) = r { return contact } if case .error(.errorAgent(.SMP(_, .AUTH))) = r { - am.showAlertMsg( - title: "Connection error (AUTH)", - message: "Sender may have deleted the connection request." - ) + await MainActor.run { showAlert( + NSLocalizedString("Connection error (AUTH)", comment: ""), + message: NSLocalizedString("Sender may have deleted the connection request.", comment: "") + ) } } else if let r { if let networkErrorAlert = networkErrorAlert(r) { - am.showAlert(networkErrorAlert) + await MainActor.run { showAlert(networkErrorAlert) } } else { logger.error("apiAcceptContactRequest error: \(String(describing: r))") - am.showAlertMsg( - title: "Error accepting contact request", - message: "Error: \(responseError(r.unexpected))" - ) + await MainActor.run { showAlert( + NSLocalizedString("Error accepting contact request", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(r.unexpected)) + ) } } } return nil @@ -1762,11 +1752,11 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws { try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId)) } -func networkErrorAlert(_ res: APIResult) -> Alert? { - if case let .error(e) = res, let alert = getNetworkErrorAlert(e) { - return mkAlert(title: alert.title, message: alert.message) +func networkErrorAlert(_ res: APIResult) -> (title: String, message: String?)? { + if case let .error(e) = res { + getNetworkErrorAlert(e) } else { - return nil + nil } } @@ -2071,7 +2061,7 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws func apiSetPublicGroupAccess(_ groupId: Int64, access: PublicGroupAccess) async throws -> GroupInfo { let r: APIResult = await chatApiSendCmd(.apiSetPublicGroupAccess(groupId: groupId, access: access)) if case let .result(.groupUpdated(_, toGroup)) = r { return toGroup } - showSetSimplexNameError(r, isChannel: true) + await showSetSimplexNameError(r, isChannel: true) throw r.unexpected } @@ -2129,7 +2119,7 @@ func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async func apiAcceptMemberContact(contactId: Int64) async -> Contact? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptMemberContact(contactId: contactId)) if case let .result(.memberContactAccepted(_, contact)) = r { return contact } - if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + if let r { await apiConnectResponseAlert(r) } return nil } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 9988b30d4e..2f76241c00 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -283,8 +283,9 @@ struct ChatInfoView: View { } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") - let a = getErrorAlert(e, "Error") - await MainActor.run { alert = .error(title: a.title, error: a.message) } + await MainActor.run { + showErrorAlert(e, NSLocalizedString("Error", comment: "")) + } } } } @@ -334,7 +335,7 @@ struct ChatInfoView: View { case .syncConnectionForceAlert: return syncConnectionForceAlert({ Task { - if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) { + if let stats = await syncContactConnection(contact, force: true) { connectionStats = stats dismiss() } @@ -541,7 +542,7 @@ struct ChatInfoView: View { private func synchronizeConnectionButton() -> some View { Button { Task { - if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) { + if let stats = await syncContactConnection(contact, force: false) { connectionStats = stats dismiss() } @@ -618,9 +619,8 @@ struct ChatInfoView: View { } } catch let error { logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing address") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error changing address", comment: "")) } } } @@ -636,9 +636,8 @@ struct ChatInfoView: View { } } catch let error { logger.error("abortSwitchContactAddress apiAbortSwitchContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error aborting address change") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error aborting address change", comment: "")) } } } @@ -748,7 +747,7 @@ struct ChatTTLOption: View { } } -func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? { +func syncContactConnection(_ contact: Contact, force: Bool) async -> ConnectionStats? { do { let stats = try apiSyncContactRatchet(contact.apiId, force) await MainActor.run { @@ -757,14 +756,8 @@ func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAler return stats } catch let error { logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - showAlert( - SomeAlert( - alert: mkAlert(title: a.title, message: a.message), - id: "syncContactConnection error" - ) - ) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } return nil } @@ -844,7 +837,7 @@ private struct CallButton: View { message: Text("Connection requires encryption renegotiation."), primaryButton: .default(Text("Fix")) { Task { - if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) { + if let stats = await syncContactConnection(contact, force: false) { connectionStats = stats } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index ec23dc15a4..bdd38cb4df 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -185,9 +185,8 @@ struct CIRcvDecryptionError: View { } } catch let error { logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } @@ -202,9 +201,8 @@ struct CIRcvDecryptionError: View { } } catch let error { logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 336b4adfd1..108f4d4306 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -174,8 +174,9 @@ struct AddGroupMembersViewCommon: View { } addedMembersCb(selectedContacts) } catch { - let a = getErrorAlert(error, "Error adding member(s)") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error adding member(s)", comment: "")) + } } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 43d23878f5..8a9bcaf059 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -155,8 +155,9 @@ struct GroupLinkView: View { do { groupLink = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole) } catch let error { - let a = getErrorAlert(error, "Error updating group link") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error updating group link", comment: "")) + } } } } @@ -188,8 +189,7 @@ struct GroupLinkView: View { logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))") await MainActor.run { creatingLink = false - let a = getErrorAlert(error, "Error creating group link") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error creating group link", comment: "")) } } } @@ -230,8 +230,7 @@ struct GroupLinkView: View { logger.error("apiAddGroupShortLink: \(responseError(error))") await MainActor.run { creatingLink = false - let a = getErrorAlert(error, "Error adding short link") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error adding short link", comment: "")) } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index c87f97089c..5e2713f922 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -278,8 +278,9 @@ struct GroupMemberInfoView: View { } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") - let a = getErrorAlert(e, "Error") - await MainActor.run { alert = .error(title: a.title, error: a.message) } + await MainActor.run { + showErrorAlert(e, NSLocalizedString("Error", comment: "")) + } } } } @@ -473,10 +474,9 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error creating member contact") await MainActor.run { progressIndicator = false - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error creating member contact", comment: "")) } } } @@ -752,8 +752,9 @@ struct GroupMemberInfoView: View { } catch let error { newRole = mem.memberRole logger.error("apiMembersRole error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing role") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error changing role", comment: "")) + } } } }, @@ -774,9 +775,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing address") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error changing address", comment: "")) } } } @@ -792,9 +792,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))") - let a = getErrorAlert(error, "Error aborting address change") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error aborting address change", comment: "")) } } } @@ -811,9 +810,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 76734dcb42..0ed78401b0 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -532,9 +532,7 @@ struct ChatListNavLink: View { .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { - AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection) { a in - AlertManager.shared.showAlertMsg(title: a.title, message: a.message) - }) + AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection)) } label: { deleteLabel } @@ -698,7 +696,7 @@ func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert { ) } -func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert { +func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, success: @escaping () -> Void = {}) -> Alert { Alert( title: Text("Delete pending connection?"), message: Text(contactConnection.displayName + "\n\n") @@ -715,7 +713,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, } } catch let error { await MainActor.run { - showError(getErrorAlert(error, "Error deleting connection")) + showErrorAlert(error, NSLocalizedString("Error deleting connection", comment: "")) } } } @@ -725,11 +723,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, } func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool, showAlert: (Alert) -> Void) async -> Bool { - let (contact, alert) = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) - if let alert = alert { - showAlert(alert) - return false - } else if let contact = contact { + if let contact = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) { await MainActor.run { ChatModel.shared.updateContact(contact) } @@ -757,8 +751,9 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { await onComplete() } catch let error { await onComplete() - let a = getErrorAlert(error, "Error joining group") - AlertManager.shared.showAlertMsg(title: a.title, message: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error joining group", comment: "")) + } } func deleteGroup() async { @@ -773,12 +768,13 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { } } -func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert { +func showErrorAlert(_ error: Error, _ title: String) { + let err = { String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(error)) } if let r = error as? ChatError, let alert = getNetworkErrorAlert(r) { - return alert + showAlert(alert.title, message: alert.message ?? err()) } else { - return ErrorAlert(title: title, message: "Error: \(responseError(error))") + showAlert(title, message: err()) } } diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 124c5ee7ba..c777eea80c 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -103,11 +103,7 @@ struct ContactConnectionInfo: View { .alert(item: $alert) { _alert in switch _alert { case .deleteInvitationAlert: - return deleteContactConnectionAlert(contactConnection) { a in - alert = .error(title: a.title, error: a.message) - } success: { - dismiss() - } + return deleteContactConnectionAlert(contactConnection, success: { dismiss() }) case let .error(title, error): return mkAlert(title: title, message: error) } } diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 82d17cd2b1..039c072c80 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -54,6 +54,10 @@ func showAlert( } } +func showAlert(_ a: (title: String, message: String?)) { + showAlert(a.title, message: a.message) +} + func showAlert( _ title: String, message: String? = nil, diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index c2154b2f27..271c3845c3 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -204,8 +204,7 @@ struct NewChatView: View { creatingConnReq = true Task { _ = try? await Task.sleep(nanoseconds: 250_000000) - let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get()) - if let (connLink, pcc) = r { + if let (connLink, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) { await MainActor.run { m.updateContactConnection(pcc) m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false) @@ -215,9 +214,6 @@ struct NewChatView: View { } else { await MainActor.run { creatingConnReq = false - if let apiAlert = apiAlert { - alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error")) - } } } } @@ -434,15 +430,9 @@ private struct ActiveProfilePicker: View { profileSwitchStatus = .idle incognitoEnabled = !incognito logger.error("apiSetConnectionIncognito error: \(responseError(error))") - let err = getErrorAlert(error, "Error changing to incognito!") - - alert = SomeAlert( - alert: Alert( - title: Text(err.title), - message: Text(err.message ?? "Error: \(responseError(error))") - ), - id: "setConnectionIncognitoError" - ) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error changing to incognito!", comment: "")) + } } } } @@ -494,14 +484,7 @@ private struct ActiveProfilePicker: View { if let currentUser = chatModel.currentUser { selectedProfile = currentUser } - let err = getErrorAlert(error, "Error changing connection profile") - alert = SomeAlert( - alert: Alert( - title: Text(err.title), - message: Text(err.message ?? "Error: \(responseError(error))") - ), - id: "changeConnectionUserError" - ) + showErrorAlert(error, NSLocalizedString("Error changing connection profile", comment: "")) } } } @@ -1331,7 +1314,7 @@ func planAndConnect( func connectTask(_ inProgress: BoxedValue) { Task { - let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress) + let result = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress) await MainActor.run { ConnectProgressManager.shared.stopConnectProgress() } @@ -1623,17 +1606,8 @@ func planAndConnect( cleanup: cleanup ) } - } else { - await MainActor.run { - if let alert { - dismissAllSheets(animated: true) { - AlertManager.shared.showAlert(alert) - cleanup?() - } - } else { - cleanup?() - } - } + } else if let cleanup { + await MainActor.run { cleanup() } } } } diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index ab84bed7df..b348057b8a 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -86,12 +86,10 @@ struct CreateSimpleXAddress: View { await MainActor.run { progressIndicator = false } } catch let error { logger.error("CreateSimpleXAddress create address: \(responseError(error))") - await MainActor.run { progressIndicator = false } - let a = getErrorAlert(error, "Error creating address") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + await MainActor.run { + progressIndicator = false + showErrorAlert(error, NSLocalizedString("Error creating address", comment: "")) + } } } } label: { @@ -156,11 +154,7 @@ struct CreateSimpleXAddress: View { } case let .failure(error): logger.error("CreateSimpleXAddress share via email: \(responseError(error))") - let a = getErrorAlert(error, "Error sending email") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + showErrorAlert(error, NSLocalizedString("Error sending email", comment: "")) } mailViewResult = nil } diff --git a/apps/ios/Shared/Views/Onboarding/YourNetwork.swift b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift index d3727e196e..015a2be491 100644 --- a/apps/ios/Shared/Views/Onboarding/YourNetwork.swift +++ b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift @@ -180,11 +180,9 @@ struct YourNetworkView: View { m.notificationMode = notificationMode } } catch let error { - let a = getErrorAlert(error, "Error enabling notifications") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error enabling notifications", comment: "")) + } } } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 01b25baed8..24ae5cffca 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -535,8 +535,7 @@ struct ConnectDesktopView: View { } private func errorAlert(_ error: Error) { - let a = getErrorAlert(error, "Error") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error", comment: "")) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 5d1bea6079..13caf135e9 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -316,9 +316,10 @@ struct UserAddressView: View { } } catch let error { logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") - let a = getErrorAlert(error, "Error creating address") - alert = .error(title: a.title, error: a.message) - await MainActor.run { progressIndicator = false } + await MainActor.run { + progressIndicator = false + showErrorAlert(error, NSLocalizedString("Error creating address", comment: "")) + } } } } @@ -390,8 +391,7 @@ struct UserAddressView: View { case .success: () case let .failure(error): logger.error("UserAddressView share via email: \(responseError(error))") - let a = getErrorAlert(error, "Error sending email") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error sending email", comment: "")) } mailViewResult = nil } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index ad3b5cdf95..3f3adbcb2d 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -319,8 +319,9 @@ struct UserProfilesView: View { } } catch let error { logger.error("Error deleting user profile: \(error)") - let a = getErrorAlert(error, "Error deleting user profile") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error deleting user profile", comment: "")) + } } func deleteUser() async throws { @@ -436,8 +437,9 @@ struct UserProfilesView: View { } } } catch let error { - let a = getErrorAlert(error, "Error updating user privacy") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error updating user privacy", comment: "")) + } } } } diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift index 2920c2383c..796b995138 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -34,20 +34,16 @@ public struct ErrorAlert: Error { } public init(_ error: any Error) { - self = if let e = error as? ChatError { - ErrorAlert(e) + self = if let chatError = error as? ChatError { + if let a = getNetworkErrorAlert(chatError) { + ErrorAlert(title: "\(a.title)", message: a.message.map { "\($0)" }) + } else { + ErrorAlert("\(chatErrorString(chatError))") + } } else { ErrorAlert("\(error.localizedDescription)") } } - - public init(_ chatError: ChatError) { - self = if let networkErrorAlert = getNetworkErrorAlert(chatError) { - networkErrorAlert - } else { - ErrorAlert("\(chatErrorString(chatError))") - } - } } extension LocalizedStringKey: @unchecked Sendable { } @@ -83,18 +79,33 @@ extension View { } } -public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? { +public func getNetworkErrorAlert(_ e: ChatError) -> (title: String, message: String?)? { switch e { case let .errorAgent(.BROKER(addr, .TIMEOUT)): - ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + ( + title: NSLocalizedString("Connection timeout", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .NETWORK(.unknownCAError))): - ErrorAlert(title: "Connection error", message: "Fingerprint in server address does not match certificate: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in server address does not match certificate: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .NETWORK)): - ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .HOST)): - ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server address is incompatible with network settings: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .TRANSPORT(.version))): - ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server version is incompatible with your app: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.SMP(serverAddress, .PROXY(proxyErr))): smpProxyErrorAlert(proxyErr, serverAddress) case let .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr)))): @@ -103,39 +114,72 @@ public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? { } } -private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? { +private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> (title: String, message: String?)? { switch proxyErr { case .BROKER(brokerErr: .TIMEOUT): - return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .NETWORK(.unknownCAError)): - return ErrorAlert(title: "Private routing error", message: "Fingerprint in forwarding server address does not match certificate: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in forwarding server address does not match certificate: %@.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .NETWORK): - return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .HOST): - return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server address is incompatible with network settings: %@.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .TRANSPORT(.version)): - return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server version is incompatible with network settings: %@.", comment: ""), serverHostname(srvAddr)) + ) default: - return nil + nil } } -private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? { +private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> (title: String, message: String?)? { switch proxyErr { case .BROKER(brokerErr: .TIMEOUT): - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .NETWORK(.unknownCAError)): - return ErrorAlert(title: "Private routing error", message: "Fingerprint in destination server address does not match certificate: \(serverHostname(relayServer)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in destination server address does not match certificate: %@.", comment: ""), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .NETWORK): - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .NO_SESSION: - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .HOST): - return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Destination server address of %@ is incompatible with forwarding server %@ settings.", comment: ""), serverHostname(relayServer), serverHostname(proxyServer)) + ) case .BROKER(brokerErr: .TRANSPORT(.version)): - return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Destination server version of %@ is incompatible with forwarding server %@.", comment: ""), serverHostname(relayServer), serverHostname(proxyServer)) + ) default: - return nil + nil } } From 8495b6fbe1827b5f2cb22ccf528d18a7f53fb122 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 3 Jul 2026 13:49:59 +0100 Subject: [PATCH 16/21] ios: export localizations --- .../bg.xcloc/Localized Contents/bg.xliff | 182 ++++++++++---- .../cs.xcloc/Localized Contents/cs.xliff | 182 ++++++++++---- .../de.xcloc/Localized Contents/de.xliff | 184 ++++++++++---- .../en.xcloc/Localized Contents/en.xliff | 227 ++++++++++++++---- .../es.xcloc/Localized Contents/es.xliff | 184 ++++++++++---- .../fi.xcloc/Localized Contents/fi.xliff | 182 ++++++++++---- .../fr.xcloc/Localized Contents/fr.xliff | 183 ++++++++++---- .../hu.xcloc/Localized Contents/hu.xliff | 184 ++++++++++---- .../it.xcloc/Localized Contents/it.xliff | 184 ++++++++++---- .../ja.xcloc/Localized Contents/ja.xliff | 182 ++++++++++---- .../nl.xcloc/Localized Contents/nl.xliff | 183 ++++++++++---- .../pl.xcloc/Localized Contents/pl.xliff | 183 ++++++++++---- .../ru.xcloc/Localized Contents/ru.xliff | 184 ++++++++++---- .../th.xcloc/Localized Contents/th.xliff | 182 ++++++++++---- .../tr.xcloc/Localized Contents/tr.xliff | 183 ++++++++++---- .../uk.xcloc/Localized Contents/uk.xliff | 183 ++++++++++---- .../Localized Contents/zh-Hans.xliff | 183 ++++++++++---- 17 files changed, 2382 insertions(+), 773 deletions(-) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 3c83ee7bf3..cac4ef3d06 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -197,6 +197,18 @@ %d месеца time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1684,11 +1696,6 @@ new chat action Промяна на режима на заключване authentication reason - - Change member role? - Промяна на ролята на члена? - No comment provided by engineer. - Change passcode Промени kодa за достъп @@ -1709,6 +1716,10 @@ new chat action Промени ролята No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Промени режима на самоунищожение @@ -2274,14 +2285,6 @@ This is your own one-time link! Свързване с настолно устройство No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Връзка @@ -2297,6 +2300,10 @@ This is your own one-time link! Връзката е блокирана No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Грешка при свързване @@ -3037,7 +3044,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. @@ -3045,7 +3052,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -3567,7 +3574,7 @@ chat item action Error Грешка при свързване със сървъра - conn error description + No comment provided by engineer. Error aborting address change @@ -3841,6 +3848,10 @@ chat item action Грешка при запазване на профила на групата No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Грешка при запазване на кода за достъп @@ -3969,6 +3980,7 @@ chat item action Error: %@ Грешка: %@ alert message +conn error description file error text snd error text @@ -5110,6 +5122,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5275,20 +5295,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Ролята на члена ще бъде променена на "%@". Членът ще получи нова покана. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5620,6 +5626,10 @@ This is your link for group %@! Име swipe action + + Name not found + No comment provided by engineer. + Network & servers Мрежа и сървъри @@ -5914,6 +5924,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5926,6 +5940,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -5934,6 +5952,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6307,8 +6329,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6491,10 +6513,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -7141,6 +7159,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Рестартирайте приложението, за да създадете нов чат профил @@ -7217,6 +7239,22 @@ swipe action Роля No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Стартиране на чат @@ -7715,6 +7753,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -8105,6 +8147,18 @@ chat item action SimpleX линковете не са разрешени No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Еднократна покана за SimpleX @@ -8533,6 +8587,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8699,6 +8769,10 @@ your contacts and groups. Те могат да бъдат променени в настройките за всеки контакт и група. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени. @@ -8864,6 +8938,10 @@ You will be prompted to complete authentication before this feature is enabled.< За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. За да разкриете своя скрит профил, въведете пълна парола в полето за търсене на страницата **Вашите чат профили**. @@ -8978,6 +9056,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -9073,18 +9155,10 @@ To connect, please ask your contact to create another connection link and check Непрочетено swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9313,6 +9387,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Потвърди кода с настолното устройство @@ -9338,6 +9416,10 @@ To connect, please ask your contact to create another connection link and check Проверете паролата на базата данни No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Провери паролата @@ -9947,6 +10029,10 @@ Repeat connection request? Вашият адрес в SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -10400,6 +10486,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator създател @@ -11014,6 +11104,10 @@ last received msg: %2$@ зачеркнат No comment provided by engineer. + + subscriber + member role + this contact този контакт diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 46a4171039..0eb4b0640c 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -197,6 +197,18 @@ %d měsíce time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1648,11 +1660,6 @@ new chat action Změnit zamykání authentication reason - - Change member role? - Změnit roli člena? - No comment provided by engineer. - Change passcode Změnit heslo @@ -1673,6 +1680,10 @@ new chat action Změnit roli No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Změnit režim sebedestrukce @@ -2186,14 +2197,6 @@ Toto je váš vlastní jednorázový odkaz! Connecting to desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Připojení @@ -2207,6 +2210,10 @@ Toto je váš vlastní jednorázový odkaz! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Chyba připojení @@ -2927,7 +2934,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. @@ -2935,7 +2942,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -3440,7 +3447,7 @@ chat item action Error Chyba - conn error description + No comment provided by engineer. Error aborting address change @@ -3711,6 +3718,10 @@ chat item action Chyba při ukládání profilu skupiny No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Chyba uložení hesla @@ -3835,6 +3846,7 @@ chat item action Error: %@ Chyba: %@ alert message +conn error description file error text snd error text @@ -4941,6 +4953,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5103,20 +5123,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Role člena se změní na "%@". Všichni členové skupiny budou upozorněni. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Role člena se změní na "%@". Člen obdrží novou pozvánku. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5435,6 +5441,10 @@ This is your link for group %@! Jméno swipe action + + Name not found + No comment provided by engineer. + Network & servers Síť a servery @@ -5725,6 +5735,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5737,6 +5751,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nikdo nesledoval vaše konverzace. Nikdo nevytvořil mapu, kde jste byli. Soukromí nikdy nebylo funkcí - byl to způsob života. @@ -5746,6 +5764,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Nejde o to mít lepší zámek na dveřích někoho jiného. Ani o to mít nájemce, který respektuje vaše soukromí, ale vede evidenci všech vašich návštěvníků. Nejste host. Jste doma. Ani král k vám nemůže vstoupit - jste suverén. @@ -6110,8 +6132,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6287,10 +6309,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6927,6 +6945,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Restartujte aplikaci pro vytvoření nového profilu chatu @@ -7002,6 +7024,22 @@ swipe action Role No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Spustit chat @@ -7492,6 +7530,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7874,6 +7916,18 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Jednorázová pozvánka SimpleX @@ -8295,6 +8349,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8462,6 +8532,10 @@ your contacts and groups. Mohou být přepsány v nastavení kontaktů. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány. @@ -8622,6 +8696,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Profily chatu**. @@ -8730,6 +8808,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8823,18 +8905,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Nepřečtený swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9057,6 +9131,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop No comment provided by engineer. @@ -9078,6 +9156,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Verify database passphrase No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase No comment provided by engineer. @@ -9661,6 +9743,10 @@ Repeat connection request? Vaše SimpleX adresa No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -10106,6 +10192,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator tvůrce @@ -10708,6 +10798,10 @@ last received msg: %2$@ stávka No comment provided by engineer. + + subscriber + member role + this contact tento kontakt diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index be4a0ce46f..3674a5bb94 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -197,6 +197,18 @@ %d Monate time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed %d Relais fehlgeschlagen @@ -1720,11 +1732,6 @@ new chat action Sperr-Modus ändern authentication reason - - Change member role? - Die Mitgliederrolle ändern? - No comment provided by engineer. - Change passcode Zugangscode ändern @@ -1745,6 +1752,10 @@ new chat action Rolle ändern No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Selbstzerstörungs-Modus ändern @@ -2335,14 +2346,6 @@ Das ist Ihr eigener Einmal-Link! Mit dem Desktop verbinden No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Verbindung @@ -2358,6 +2361,10 @@ Das ist Ihr eigener Einmal-Link! Verbindung blockiert No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Verbindungsfehler @@ -3147,7 +3154,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. Adresse des Zielservers von %@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %@. No comment provided by engineer. @@ -3157,7 +3164,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. Die Version des Zielservers %@ ist nicht kompatibel mit dem Weiterleitungsserver %@. No comment provided by engineer. @@ -3714,7 +3721,7 @@ chat item action Error Fehler - conn error description + No comment provided by engineer. Error aborting address change @@ -4014,6 +4021,10 @@ chat item action Fehler beim Speichern des Gruppenprofils No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Fehler beim Speichern des Zugangscodes @@ -4148,6 +4159,7 @@ chat item action Error: %@ Fehler: %@ alert message +conn error description file error text snd error text @@ -5371,6 +5383,14 @@ Das ist Ihr Link für die Gruppe %@! Weniger Datenverkehr in mobilen Netzen. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you Jemand mit Ihnen verbinden lassen @@ -5551,21 +5571,6 @@ Das ist Ihr Link für die Gruppe %@! Mitglieder-Meldungen chat feature - - Member role will be changed to "%@". All chat members will be notified. - Die Rolle des Mitglieds wird auf "%@" geändert. Alle Chat-Mitglieder werden darüber informiert. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Die Mitgliederrolle wird auf "%@" geändert. Alle Mitglieder der Gruppe werden benachrichtigt. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Die Mitgliederrolle wird auf "%@" geändert. Das Mitglied wird eine neue Einladung erhalten. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden! @@ -5925,6 +5930,10 @@ Das ist Ihr Link für die Gruppe %@! Name swipe action + + Name not found + No comment provided by engineer. + Network & servers Netzwerk & Server @@ -6257,6 +6266,10 @@ Die sicherste Verschlüsselung. Keine Server für den Empfang von Nachrichten. servers error + + No servers to resolve names. + servers warning + No servers to send files. Keine Server für das Versenden von Dateien. @@ -6272,6 +6285,10 @@ Die sicherste Verschlüsselung. Keine ungelesenen Chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature - sie war selbstverständlich. @@ -6282,6 +6299,10 @@ Die sicherste Verschlüsselung. Non‑Profit‑Governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten - Sie sind souverän. @@ -6700,9 +6721,8 @@ alert button Eigentümer No comment provided by engineer. - - Owners - Eigentümer + + Owners & contributors No comment provided by engineer. @@ -6894,10 +6914,6 @@ Fehler: %@ Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können. @@ -7618,6 +7634,10 @@ swipe action Auf das Benutzer-spezifische Design zurücksetzen No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Um ein neues Chat-Profil zu erstellen, starten Sie die App neu @@ -7698,6 +7718,22 @@ swipe action Rolle No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Chat starten @@ -8242,6 +8278,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Der Server wurde dem Betreiber %@ hinzugefügt. @@ -8678,6 +8718,18 @@ chat item action SimpleX-Links sind nicht erlaubt No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX-Einmal-Einladung @@ -9154,6 +9206,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. Die Adresse wird gekürzt sein, und Ihr Profil wird über die Adresse geteilt. @@ -9340,6 +9408,10 @@ in dem Sie Ihre Kontakte und Gruppen besitzen. Sie können in den Kontakteinstellungen überschrieben werden. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Dieser Vorgang kann nicht rückgängig gemacht werden - alle empfangenen und gesendeten Dateien sowie Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. @@ -9520,6 +9592,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite **Ihre Chat-Profile** ein, um Ihr verborgenes Profil zu sehen. @@ -9645,6 +9721,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Abonnent für alle freigeben? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Nicht ausgelieferte Nachrichten @@ -9742,19 +9822,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Ungelesen swipe action - - Unsupported channel name - alert title - Unsupported connection link Verbindungs-Link wird nicht unterstützt conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -10013,6 +10085,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Überprüfen relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Code mit dem Desktop überprüfen @@ -10038,6 +10114,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Überprüfen Sie das Datenbank-Passwort No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Überprüfen Sie das Passwort @@ -10687,6 +10767,10 @@ Verbindungsanfrage wiederholen? Ihre SimpleX-Adresse No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Ihr geschäftlicher Kontakt @@ -11174,6 +11258,10 @@ marked deleted chat item preview text Kontakt sollte annehmen… No comment provided by engineer. + + contributor + member role + creator Ersteller @@ -11826,6 +11914,10 @@ Zuletzt empfangene Nachricht: %2$@ durchstreichen No comment provided by engineer. + + subscriber + member role + this contact Dieser Kontakt diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index d375d20396..fe8baaaa41 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -200,6 +200,21 @@ %d months time interval + + %d owner + %d owner + channel owners count + + + %d owners + %d owners + channel owners count + + + %d owners & contributors + %d owners & contributors + channel members count + %d relays failed %d relays failed @@ -1735,11 +1750,6 @@ new chat action Change lock mode authentication reason - - Change member role? - Change member role? - No comment provided by engineer. - Change passcode Change passcode @@ -1760,6 +1770,11 @@ new chat action Change role No comment provided by engineer. + + Change role? + Change role? + No comment provided by engineer. + Change self-destruct mode Change self-destruct mode @@ -2353,16 +2368,6 @@ This is your own one-time link! Connecting to desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - Connecting via contact name requires a newer app version. - alert message - Connection Connection @@ -2378,6 +2383,11 @@ This is your own one-time link! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + Connection blocked: %@ + conn error description + Connection error Connection error @@ -3171,8 +3181,8 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. @@ -3181,8 +3191,8 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -3739,7 +3749,7 @@ chat item action Error Error - conn error description + No comment provided by engineer. Error aborting address change @@ -4041,6 +4051,11 @@ chat item action Error saving group profile No comment provided by engineer. + + Error saving name + Error saving name + alert title + Error saving passcode Error saving passcode @@ -4175,6 +4190,7 @@ chat item action Error: %@ Error: %@ alert message +conn error description file error text snd error text @@ -5401,6 +5417,16 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you Let someone connect to you @@ -5581,21 +5607,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Member role will be changed to "%@". All group members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Member role will be changed to "%@". The member will receive a new invitation. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Member will be removed from chat - this cannot be undone! @@ -5956,6 +5967,11 @@ This is your link for group %@! Name swipe action + + Name not found + Name not found + No comment provided by engineer. + Network & servers Network & servers @@ -6290,6 +6306,11 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + No servers to resolve names. + servers warning + No servers to send files. No servers to send files. @@ -6305,6 +6326,11 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. @@ -6315,6 +6341,11 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. @@ -6734,9 +6765,9 @@ alert button Owner No comment provided by engineer. - - Owners - Owners + + Owners & contributors + Owners & contributors No comment provided by engineer. @@ -6928,11 +6959,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Please wait for group moderators to review your request to join the group. @@ -7657,6 +7683,11 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Restart the app to create a new chat profile @@ -7737,6 +7768,26 @@ swipe action Role No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Run chat @@ -8283,6 +8334,11 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Server added to operator %@. @@ -8719,6 +8775,21 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + SimpleX name + No comment provided by engineer. + + + SimpleX name error + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX one-time invitation @@ -9197,6 +9268,26 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. The address will be short, and your profile will be shared via the address. @@ -9384,6 +9475,11 @@ your contacts and groups. They can be overridden in contact and group settings. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. @@ -9567,6 +9663,11 @@ You will be prompted to complete authentication before this feature is enabled.< To record voice message please grant permission to use Microphone. No comment provided by engineer. + + To resolve names + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. @@ -9692,6 +9793,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + Unconfirmed name + No comment provided by engineer. + Undelivered messages Undelivered messages @@ -9789,21 +9895,11 @@ To connect, please ask your contact to create another connection link and check Unread swipe action - - Unsupported channel name - Unsupported channel name - alert title - Unsupported connection link Unsupported connection link conn error description - - Unsupported contact name - Unsupported contact name - alert title - Unverified badge Unverified badge @@ -10064,6 +10160,11 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Verify code with desktop @@ -10089,6 +10190,11 @@ To connect, please ask your contact to create another connection link and check Verify database passphrase No comment provided by engineer. + + Verify name + Verify name + No comment provided by engineer. + Verify passphrase Verify passphrase @@ -10742,6 +10848,11 @@ Repeat connection request? Your SimpleX address No comment provided by engineer. + + Your SimpleX name + Your SimpleX name + No comment provided by engineer. + Your business contact Your business contact @@ -11232,6 +11343,11 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + contributor + member role + creator creator @@ -11885,6 +12001,11 @@ last received msg: %2$@ strike No comment provided by engineer. + + subscriber + subscriber + member role + this contact this contact diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 8ed8087d1b..88294ced4e 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -197,6 +197,18 @@ %d mes(es) time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed %d servidores han fallado @@ -1720,11 +1732,6 @@ new chat action Cambiar el modo de bloqueo authentication reason - - Change member role? - ¿Cambiar rol? - No comment provided by engineer. - Change passcode Cambiar código de acceso @@ -1745,6 +1752,10 @@ new chat action Cambiar rol No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Cambiar el modo de autodestrucción @@ -2335,14 +2346,6 @@ This is your own one-time link! Conectando con ordenador No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Conexión @@ -2358,6 +2361,10 @@ This is your own one-time link! Conexión bloqueada No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Error conexión @@ -3147,7 +3154,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. La dirección del servidor de destino de %@ es incompatible con la configuración del servidor de reenvío %@. No comment provided by engineer. @@ -3157,7 +3164,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. La versión del servidor de destino de %@ es incompatible con el servidor de reenvío %@. No comment provided by engineer. @@ -3714,7 +3721,7 @@ chat item action Error Error - conn error description + No comment provided by engineer. Error aborting address change @@ -4014,6 +4021,10 @@ chat item action Error al guardar perfil de grupo No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Error al guardar código de acceso @@ -4148,6 +4159,7 @@ chat item action Error: %@ Error: %@ alert message +conn error description file error text snd error text @@ -5371,6 +5383,14 @@ This is your link for group %@! Menos tráfico en redes móviles. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you Conecta con alguien @@ -5551,21 +5571,6 @@ This is your link for group %@! Informes de miembros chat feature - - Member role will be changed to "%@". All chat members will be notified. - El rol del miembro cambiará a "%@". Se notificará en el chat. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - El rol del miembro cambiará a "%@" y se notificará al grupo. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - El rol del miembro cambiará a "%@" y recibirá una invitación nueva. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! El miembro será eliminado del chat. ¡No puede deshacerse! @@ -5925,6 +5930,10 @@ This is your link for group %@! Nombre swipe action + + Name not found + No comment provided by engineer. + Network & servers Servidores y Redes @@ -6257,6 +6266,10 @@ El cifrado más seguro. Sin servidores para recibir mensajes. servers error + + No servers to resolve names. + servers warning + No servers to send files. Sin servidores para enviar archivos. @@ -6272,6 +6285,10 @@ El cifrado más seguro. Ningún chat sin leer No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nadie monitorizaba tus conversaciones. Nadie registraba tus ubicaciones. La privacidad nunca fue un lujo, era la manera de vivir. @@ -6282,6 +6299,10 @@ El cifrado más seguro. Gobernanza no lucrativa No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No un candado mejorado en la puerta de otro. No un terrateniente que respeta tu privacidad pero sigue guardando un registro de tus visitantes. Tu no eres el invitado. Estás en tu casa y ningún rey podrá entrar. Tu eres el soberano. @@ -6700,9 +6721,8 @@ alert button Propietario No comment provided by engineer. - - Owners - Propietarios + + Owners & contributors No comment provided by engineer. @@ -6894,10 +6914,6 @@ Error: %@ Por favor, intenta desactivar y reactivar las notificaciones. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo. @@ -7618,6 +7634,10 @@ swipe action Restablecer al tema del usuario No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Reinicia la aplicación para crear un perfil nuevo @@ -7698,6 +7718,22 @@ swipe action Rol No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Ejecutar SimpleX @@ -8242,6 +8278,10 @@ chat item action Servidor No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Servidor añadido al operador %@. @@ -8678,6 +8718,18 @@ chat item action Enlaces SimpleX no permitidos No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Invitación SimpleX de un uso @@ -9154,6 +9206,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. La dirección pasará a ser corta y tu perfil será compartido mediante la dirección. @@ -9340,6 +9408,10 @@ y los contactos son tuyos. Se puede modificar desde la configuración particular de cada grupo y contacto. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán. @@ -9520,6 +9592,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.Para grabar el mensaje de voz concede permiso para usar el micrófono. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**. @@ -9645,6 +9721,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.¿Desbloquear al suscriptor para todos? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Mensajes no entregados @@ -9742,19 +9822,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión No leído swipe action - - Unsupported channel name - alert title - Unsupported connection link Enlace de conexión no compatible conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -10013,6 +10085,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Verificar relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Verificar código con ordenador @@ -10038,6 +10114,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Verificar la contraseña de la base de datos No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Verificar frase de contraseña @@ -10687,6 +10767,10 @@ Repeat connection request? Mi dirección SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Mi contacto empresarial @@ -11174,6 +11258,10 @@ marked deleted chat item preview text el contacto debe aceptarte… No comment provided by engineer. + + contributor + member role + creator creador @@ -11826,6 +11914,10 @@ last received msg: %2$@ tachado No comment provided by engineer. + + subscriber + member role + this contact este contacto diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index d1300df5ed..cf32c10c6d 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -184,6 +184,18 @@ %d kuukautta time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1542,11 +1554,6 @@ new chat action Vaihda lukitustilaa authentication reason - - Change member role? - Vaihda jäsenroolia? - No comment provided by engineer. - Change passcode Vaihda pääsykoodi @@ -1567,6 +1574,10 @@ new chat action Vaihda rooli No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Vaihda itsetuhotilaa @@ -2073,14 +2084,6 @@ This is your own one-time link! Connecting to desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Yhteys @@ -2094,6 +2097,10 @@ This is your own one-time link! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Yhteysvirhe @@ -2814,7 +2821,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. @@ -2822,7 +2829,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -3326,7 +3333,7 @@ chat item action Error Virhe - conn error description + No comment provided by engineer. Error aborting address change @@ -3596,6 +3603,10 @@ chat item action Virhe ryhmäprofiilin tallentamisessa No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Virhe pääsykoodin tallentamisessa @@ -3719,6 +3730,7 @@ chat item action Error: %@ Virhe: %@ alert message +conn error description file error text snd error text @@ -4825,6 +4837,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -4987,20 +5007,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Jäsenen rooli muuttuu muotoon "%@". Kaikille ryhmän jäsenille ilmoitetaan asiasta. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Jäsenen rooli muutetaan muotoon "%@". Jäsen saa uuden kutsun. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5319,6 +5325,10 @@ This is your link for group %@! Nimi swipe action + + Name not found + No comment provided by engineer. + Network & servers Verkko ja palvelimet @@ -5608,6 +5618,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5620,6 +5634,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -5628,6 +5646,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -5990,8 +6012,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6167,10 +6189,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6807,6 +6825,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Käynnistä sovellus uudelleen uuden keskusteluprofiilin luomiseksi @@ -6882,6 +6904,22 @@ swipe action Rooli No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Käynnistä chat @@ -7371,6 +7409,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7753,6 +7795,18 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX-kertakutsu @@ -8173,6 +8227,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8337,6 +8407,10 @@ your contacts and groups. Ne voidaan ohittaa kontakti- ja ryhmäasetuksissa. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Tätä toimintoa ei voi kumota - kaikki vastaanotetut ja lähetetyt tiedostot ja media poistetaan. Matalan resoluution kuvat säilyvät. @@ -8497,6 +8571,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Voit paljastaa piilotetun profiilisi syöttämällä koko salasanan hakukenttään **Keskusteluprofiilisi** -sivulla. @@ -8604,6 +8682,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8697,18 +8779,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Lukematon swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -8931,6 +9005,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop No comment provided by engineer. @@ -8952,6 +9030,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Verify database passphrase No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase No comment provided by engineer. @@ -9534,6 +9616,10 @@ Repeat connection request? SimpleX-osoitteesi No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -9978,6 +10064,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator luoja @@ -10580,6 +10670,10 @@ last received msg: %2$@ soita No comment provided by engineer. + + subscriber + member role + this contact tämä kontakti diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 2d180fd51e..b743c5da98 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -197,6 +197,18 @@ %d mois time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1677,11 +1689,6 @@ new chat action Modifier le mode de verrouillage authentication reason - - Change member role? - Changer le rôle du membre ? - No comment provided by engineer. - Change passcode Modifier le code d'accès @@ -1702,6 +1709,10 @@ new chat action Changer le rôle No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Modifier le mode d'autodestruction @@ -2262,14 +2273,6 @@ Il s'agit de votre propre lien unique ! Connexion au bureau No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Connexion @@ -2285,6 +2288,10 @@ Il s'agit de votre propre lien unique ! Connexion bloquée No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Erreur de connexion @@ -3056,7 +3063,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. L'adresse du serveur de destination %@ est incompatible avec les paramètres du serveur de redirection %@. No comment provided by engineer. @@ -3066,7 +3073,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. La version du serveur de destination %@ est incompatible avec le serveur de redirection %@. No comment provided by engineer. @@ -3610,7 +3617,7 @@ chat item action Error Erreur - conn error description + No comment provided by engineer. Error aborting address change @@ -3901,6 +3908,10 @@ chat item action Erreur lors de la sauvegarde du profil de groupe No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Erreur lors de la sauvegarde du code d'accès @@ -4033,6 +4044,7 @@ chat item action Error: %@ Erreur : %@ alert message +conn error description file error text snd error text @@ -5221,6 +5233,14 @@ Voici votre lien pour le groupe %@ ! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5389,21 +5409,6 @@ Voici votre lien pour le groupe %@ ! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - Le rôle du membre sera modifié pour « %@ ». Tous les membres du chat seront notifiés. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Le rôle du membre sera changé pour "%@". Tous les membres du groupe en seront informés. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Le rôle du membre sera changé pour "%@". Ce membre recevra une nouvelle invitation. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Le membre sera retiré de la discussion - cela ne peut pas être annulé ! @@ -5750,6 +5755,10 @@ Voici votre lien pour le groupe %@ ! Nom swipe action + + Name not found + No comment provided by engineer. + Network & servers Réseau et serveurs @@ -6062,6 +6071,10 @@ The most secure encryption. Pas de serveurs pour recevoir des messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. Pas de serveurs pour envoyer des fichiers. @@ -6075,6 +6088,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -6083,6 +6100,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6469,8 +6490,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6659,10 +6680,6 @@ Erreur : %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -7341,6 +7358,10 @@ swipe action Réinitialisation au thème de l'utilisateur No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Redémarrez l'application pour créer un nouveau profil de chat @@ -7418,6 +7439,22 @@ swipe action Rôle No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Exécuter le chat @@ -7939,6 +7976,10 @@ chat item action Serveur No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Serveur ajouté à l'opérateur %@. @@ -8356,6 +8397,18 @@ chat item action Les liens SimpleX ne sont pas autorisés No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Invitation unique SimpleX @@ -8803,6 +8856,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8980,6 +9049,10 @@ your contacts and groups. Ils peuvent être modifiés dans les paramètres des contacts et des groupes. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Cette action ne peut être annulée - tous les fichiers et médias reçus et envoyés seront supprimés. Les photos à faible résolution seront conservées. @@ -9152,6 +9225,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page **Vos profils de chat**. @@ -9271,6 +9348,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Messages non distribués @@ -9368,18 +9449,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Non lu swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9622,6 +9695,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Vérifier le code avec le bureau @@ -9647,6 +9724,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Vérifier la phrase secrète de la base de données No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Vérifier la phrase secrète @@ -10277,6 +10358,10 @@ Répéter la demande de connexion ? Votre adresse SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -10738,6 +10823,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator créateur @@ -11365,6 +11454,10 @@ dernier message reçu : %2$@ barré No comment provided by engineer. + + subscriber + member role + this contact ce contact diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index c0ddfedc0b..cbbda85b5d 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -197,6 +197,18 @@ %d hónap time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed %d átjátszóhoz nem sikerült kapcsolódni @@ -1720,11 +1732,6 @@ new chat action Zárolási mód módosítása authentication reason - - Change member role? - Módosítja a tag szerepkörét? - No comment provided by engineer. - Change passcode Jelkód módosítása @@ -1745,6 +1752,10 @@ new chat action Szerepkör módosítása No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Önmegsemmisítő-mód módosítása @@ -2335,14 +2346,6 @@ Ez a saját egyszer használható meghívója! Társítás számítógéppel No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Kapcsolat @@ -2358,6 +2361,10 @@ Ez a saját egyszer használható meghívója! A kapcsolat le van tiltva No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Kapcsolódási hiba @@ -3147,7 +3154,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival. No comment provided by engineer. @@ -3157,7 +3164,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval. No comment provided by engineer. @@ -3714,7 +3721,7 @@ chat item action Error Hiba - conn error description + No comment provided by engineer. Error aborting address change @@ -4014,6 +4021,10 @@ chat item action Hiba történt a csoportprofil mentésekor No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Hiba történt a jelkód mentésekor @@ -4148,6 +4159,7 @@ chat item action Error: %@ Hiba: %@ alert message +conn error description file error text snd error text @@ -5371,6 +5383,14 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Kevesebb adatforgalom a mobilhálózatokon. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you Legyen elérhető mások számára @@ -5551,21 +5571,6 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Tagok jelentései chat feature - - Member role will be changed to "%@". All chat members will be notified. - A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza! @@ -5925,6 +5930,10 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Név swipe action + + Name not found + No comment provided by engineer. + Network & servers Hálózat és kiszolgálók @@ -6257,6 +6266,10 @@ A legbiztonságosabb titkosítás. Nincsenek üzenetfogadási kiszolgálók. servers error + + No servers to resolve names. + servers warning + No servers to send files. Nincsenek fájlküldési kiszolgálók. @@ -6272,6 +6285,10 @@ A legbiztonságosabb titkosítás. Nincsenek olvasatlan csevegések No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Senki sem követte nyomon a beszélgetéseinket. Senki sem készített térképet arról, hogy merre jártunk. A magánéletünk nem csak egy funkció volt, hanem az életmódunk. @@ -6282,6 +6299,10 @@ A legbiztonságosabb titkosítás. Nonprofit irányítás No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide - Ön itt szuverén. @@ -6700,9 +6721,8 @@ alert button Tulajdonos No comment provided by engineer. - - Owners - Tulajdonosok + + Owners & contributors No comment provided by engineer. @@ -6894,10 +6914,6 @@ Hiba: %@ Próbálja meg letiltani és újra engedélyezni az értesítéseket. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérését. @@ -7618,6 +7634,10 @@ swipe action Felhasználó által létrehozott téma visszaállítása No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Új csevegési profil létrehozásához indítsa újra az alkalmazást @@ -7698,6 +7718,22 @@ swipe action Szerepkör No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Csevegési szolgáltatás indítása @@ -8242,6 +8278,10 @@ chat item action Kiszolgáló No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Kiszolgáló hozzáadva a következő üzemeltetőhöz: %@. @@ -8678,6 +8718,18 @@ chat item action A SimpleX-hivatkozások küldése le van tiltva No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Egyszer használható SimpleX meghívó @@ -9154,6 +9206,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. A cím rövid lesz és a profil meg lesz osztva a címen keresztül. @@ -9340,6 +9408,10 @@ a saját kapcsolatait és csoportjait. Ezek felülbírálhatók a partner- és csoportbeállításokban. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Ez a művelet nem vonható vissza – az összes fogadott és küldött fájl a médiatartalmakkal együtt törölve lesznek. Az alacsony felbontású képek viszont megmaradnak. @@ -9520,6 +9592,10 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben. @@ -9645,6 +9721,10 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Az összes feliratkozó számára feloldja a feliratkozó letiltását? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Kézbesítetlen üzenetek @@ -9742,19 +9822,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Olvasatlan swipe action - - Unsupported channel name - alert title - Unsupported connection link Nem támogatott kapcsolattartási hivatkozás conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -10013,6 +10085,10 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Ellenőrzés relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Kód ellenőrzése a számítógépen @@ -10038,6 +10114,10 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Adatbázis jelmondatának ellenőrzése No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Jelmondat ellenőrzése @@ -10687,6 +10767,10 @@ Megismétli a kapcsolódási kérést? Profil SimpleX-címe No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Üzleti partner @@ -11174,6 +11258,10 @@ marked deleted chat item preview text a partnernek el kell fogadnia… No comment provided by engineer. + + contributor + member role + creator készítő @@ -11826,6 +11914,10 @@ utoljára fogadott üzenet: %2$@ áthúzott No comment provided by engineer. + + subscriber + member role + this contact ez a partner diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index ae720efdeb..2626efcd5d 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -197,6 +197,18 @@ %d mesi time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed %d relay falliti @@ -1720,11 +1732,6 @@ new chat action Cambia modalità di blocco authentication reason - - Change member role? - Cambiare ruolo del membro? - No comment provided by engineer. - Change passcode Cambia codice di accesso @@ -1745,6 +1752,10 @@ new chat action Cambia ruolo No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Cambia modalità di autodistruzione @@ -2335,14 +2346,6 @@ Questo è il tuo link una tantum! Connessione al desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Connessione @@ -2358,6 +2361,10 @@ Questo è il tuo link una tantum! Connessione bloccata No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Errore di connessione @@ -3147,7 +3154,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. L'indirizzo del server di destinazione di %@ è incompatibile con le impostazioni del server di inoltro %@. No comment provided by engineer. @@ -3157,7 +3164,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. La versione del server di destinazione di %@ è incompatibile con il server di inoltro %@. No comment provided by engineer. @@ -3714,7 +3721,7 @@ chat item action Error Errore - conn error description + No comment provided by engineer. Error aborting address change @@ -4014,6 +4021,10 @@ chat item action Errore nel salvataggio del profilo del gruppo No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Errore nel salvataggio del codice di accesso @@ -4148,6 +4159,7 @@ chat item action Error: %@ Errore: %@ alert message +conn error description file error text snd error text @@ -5371,6 +5383,14 @@ Questo è il tuo link per il gruppo %@! Meno traffico sulle reti mobili. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you Lascia che qualcuno si connetta a te @@ -5551,21 +5571,6 @@ Questo è il tuo link per il gruppo %@! Segnalazioni dei membri chat feature - - Member role will be changed to "%@". All chat members will be notified. - Il ruolo del membro verrà cambiato in "%@". Verranno notificati tutti i membri della chat. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Il ruolo del membro verrà cambiato in "%@". Tutti i membri del gruppo verranno avvisati. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Il ruolo del membro verrà cambiato in "%@". Il membro riceverà un invito nuovo. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Il membro verrà rimosso dalla chat, non è reversibile! @@ -5925,6 +5930,10 @@ Questo è il tuo link per il gruppo %@! Nome swipe action + + Name not found + No comment provided by engineer. + Network & servers Rete e server @@ -6257,6 +6266,10 @@ La crittografia più sicura. Nessun server per ricevere messaggi. servers error + + No servers to resolve names. + servers warning + No servers to send files. Nessun server per inviare file. @@ -6272,6 +6285,10 @@ La crittografia più sicura. Nessuna chat non letta No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai stata una caratteristica, era uno stile di vita. @@ -6282,6 +6299,10 @@ La crittografia più sicura. Organizzazione non a scopo di lucro No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Non una serratura migliore sulla porta di qualcun altro. Non un padrone di casa più gentile che rispetta la tua privacy, ma che continua a tenere traccia di tutti i visitatori. Non sei un ospite. Sei a casa tua. Nessun re può entrarvi: sei tu il sovrano. @@ -6700,9 +6721,8 @@ alert button Proprietario No comment provided by engineer. - - Owners - Proprietari + + Owners & contributors No comment provided by engineer. @@ -6894,10 +6914,6 @@ Errore: %@ Prova a disattivare e riattivare le notifiche. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo. @@ -7618,6 +7634,10 @@ swipe action Ripristina al tema dell'utente No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Riavvia l'app per creare un nuovo profilo di chat @@ -7698,6 +7718,22 @@ swipe action Ruolo No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Avvia chat @@ -8242,6 +8278,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Server aggiunto all'operatore %@. @@ -8678,6 +8718,18 @@ chat item action Link di SimpleX non consentiti No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Invito SimpleX una tantum @@ -9154,6 +9206,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. L'indirizzo sarà breve e il tuo profilo verrà condiviso attraverso l'indirizzo. @@ -9340,6 +9408,10 @@ i tuoi contatti e i tuoi gruppi. Possono essere sovrascritte nelle impostazioni dei contatti e dei gruppi. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Questa azione non può essere annullata: tutti i file e i media ricevuti e inviati verranno eliminati. Rimarranno le immagini a bassa risoluzione. @@ -9520,6 +9592,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per registrare un messaggio vocale, concedi l'autorizzazione all'uso del microfono. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina **I tuoi profili di chat**. @@ -9645,6 +9721,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Sbloccare l'iscritto per tutti? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Messaggi non consegnati @@ -9742,19 +9822,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Non letto swipe action - - Unsupported channel name - alert title - Unsupported connection link Link di connessione non supportato conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -10013,6 +10085,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Verifica relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Verifica il codice con il desktop @@ -10038,6 +10114,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Verifica password del database No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Verifica password @@ -10687,6 +10767,10 @@ Ripetere la richiesta di connessione? Il tuo indirizzo SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Il tuo contatto lavorativo @@ -11174,6 +11258,10 @@ marked deleted chat item preview text il contatto dovrebbe accettare… No comment provided by engineer. + + contributor + member role + creator creatore @@ -11826,6 +11914,10 @@ ultimo msg ricevuto: %2$@ barrato No comment provided by engineer. + + subscriber + member role + this contact questo contatto diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 4c5970f087..b8e1497bba 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -197,6 +197,18 @@ %d 月 time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1617,11 +1629,6 @@ new chat action ロックモードを変更 authentication reason - - Change member role? - メンバーの役割を変更しますか? - No comment provided by engineer. - Change passcode パスコードを変更 @@ -1642,6 +1649,10 @@ new chat action 役割変更 No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode 自己破壊モードの変更 @@ -2166,14 +2177,6 @@ This is your own one-time link! デスクトップに接続中 No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection 接続 @@ -2188,6 +2191,10 @@ This is your own one-time link! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error 接続エラー @@ -2913,7 +2920,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. @@ -2921,7 +2928,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -3433,7 +3440,7 @@ chat item action Error エラー - conn error description + No comment provided by engineer. Error aborting address change @@ -3703,6 +3710,10 @@ chat item action グループのプロフィール保存にエラー発生 No comment provided by engineer. + + Error saving name + alert title + Error saving passcode パスコードの保存にエラー発生 @@ -3826,6 +3837,7 @@ chat item action Error: %@ エラー : %@ alert message +conn error description file error text snd error text @@ -4932,6 +4944,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5094,20 +5114,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - メンバーの役割が "%@" に変更されます。 グループメンバー全員に通知されます。 - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - メンバーの役割が "%@" に変更されます。 メンバーは新たな招待を受け取ります。 - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5427,6 +5433,10 @@ This is your link for group %@! 名前 swipe action + + Name not found + No comment provided by engineer. + Network & servers ネットワークとサーバ @@ -5717,6 +5727,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5729,6 +5743,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -5737,6 +5755,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6100,8 +6122,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6277,10 +6299,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6917,6 +6935,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile 新しいチャットプロファイルを作成するためにアプリを再起動する @@ -6992,6 +7014,22 @@ swipe action 役割 No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat チャット起動 @@ -7474,6 +7512,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7856,6 +7898,18 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX使い捨て招待リンク @@ -8277,6 +8331,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8441,6 +8511,10 @@ your contacts and groups. これらは連絡先の設定が優先します。 No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. ファイルとメディアが全て削除されます (※元に戻せません※)。低解像度の画像が残ります。 @@ -8600,6 +8674,10 @@ You will be prompted to complete authentication before this feature is enabled.< 音声メッセージを録音する場合は、マイクの使用を許可してください。 No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. 非表示のプロフィールを表示するには、**チャット プロフィール** ページの検索フィールドに完全なパスワードを入力します。 @@ -8707,6 +8785,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8800,18 +8882,10 @@ To connect, please ask your contact to create another connection link and check 未読 swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9034,6 +9108,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop No comment provided by engineer. @@ -9055,6 +9133,10 @@ To connect, please ask your contact to create another connection link and check Verify database passphrase No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase No comment provided by engineer. @@ -9638,6 +9720,10 @@ Repeat connection request? あなたのSimpleXアドレス No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -10082,6 +10168,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator 作成者 @@ -10685,6 +10775,10 @@ last received msg: %2$@ 取り消し線 No comment provided by engineer. + + subscriber + member role + this contact この連絡先 diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 26dec5caf6..78a4b80039 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -197,6 +197,18 @@ %d maanden time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1674,11 +1686,6 @@ new chat action Wijzig de vergrendelings modus authentication reason - - Change member role? - Rol van lid wijzigen? - No comment provided by engineer. - Change passcode Toegangscode wijzigen @@ -1699,6 +1706,10 @@ new chat action Rol wijzigen No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Zelfvernietigings modus wijzigen @@ -2262,14 +2273,6 @@ Dit is uw eigen eenmalige link! Verbinding maken met desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Verbinding @@ -2285,6 +2288,10 @@ Dit is uw eigen eenmalige link! Verbinding geblokkeerd No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Verbindingsfout @@ -3057,7 +3064,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. Het bestemmingsserveradres van %@ is niet compatibel met de doorstuurserverinstellingen %@. No comment provided by engineer. @@ -3067,7 +3074,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. De versie van de bestemmingsserver %@ is niet compatibel met de doorstuurserver %@. No comment provided by engineer. @@ -3611,7 +3618,7 @@ chat item action Error Fout - conn error description + No comment provided by engineer. Error aborting address change @@ -3904,6 +3911,10 @@ chat item action Fout bij opslaan van groep profiel No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Fout bij opslaan van toegangscode @@ -4036,6 +4047,7 @@ chat item action Error: %@ Fout: %@ alert message +conn error description file error text snd error text @@ -5237,6 +5249,14 @@ Dit is jouw link voor groep %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5410,21 +5430,6 @@ Dit is jouw link voor groep %@! Ledenrapporten chat feature - - Member role will be changed to "%@". All chat members will be notified. - De rol van het lid wordt gewijzigd naar "%@". Alle chatleden worden op de hoogte gebracht. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - De rol van lid wordt gewijzigd in "%@". Alle groepsleden worden op de hoogte gebracht. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - De rol van lid wordt gewijzigd in "%@". Het lid ontvangt een nieuwe uitnodiging. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt! @@ -5777,6 +5782,10 @@ Dit is jouw link voor groep %@! Naam swipe action + + Name not found + No comment provided by engineer. + Network & servers Netwerk & servers @@ -6096,6 +6105,10 @@ The most secure encryption. Geen servers om berichten te ontvangen. servers error + + No servers to resolve names. + servers warning + No servers to send files. Geen servers om bestanden te verzenden. @@ -6111,6 +6124,10 @@ The most secure encryption. Geen ongelezen chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -6119,6 +6136,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6512,8 +6533,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6703,10 +6724,6 @@ Fout: %@ Probeer meldingen uit en weer in te schakelen. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Wacht totdat de moderators van de groep uw verzoek tot lidmaatschap van de groep hebben beoordeeld. @@ -7406,6 +7423,10 @@ swipe action Terugzetten naar gebruikersthema No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Start de app opnieuw om een nieuw chatprofiel aan te maken @@ -7485,6 +7506,22 @@ swipe action Rol No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Chat uitvoeren @@ -8009,6 +8046,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Server toegevoegd aan operator %@. @@ -8431,6 +8472,18 @@ chat item action SimpleX-links zijn niet toegestaan No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Eenmalige SimpleX uitnodiging @@ -8882,6 +8935,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -9059,6 +9128,10 @@ your contacts and groups. Ze kunnen worden overschreven in contactinstellingen No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Deze actie kan niet ongedaan worden gemaakt, alle ontvangen en verzonden bestanden en media worden verwijderd. Foto's met een lage resolutie blijven behouden. @@ -9234,6 +9307,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Geef toestemming om de microfoon te gebruiken om een spraakbericht op te nemen. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoek veld in op de pagina **Uw chatprofielen**. @@ -9354,6 +9431,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Niet afgeleverde berichten @@ -9451,19 +9532,11 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Ongelezen swipe action - - Unsupported channel name - alert title - Unsupported connection link Niet-ondersteunde verbindingslink conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9710,6 +9783,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Code verifiëren met desktop @@ -9735,6 +9812,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Controleer het wachtwoord van de database No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Controleer het wachtwoord @@ -10367,6 +10448,10 @@ Verbindingsverzoek herhalen? Uw SimpleX adres No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -10836,6 +10921,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator creator @@ -11475,6 +11564,10 @@ laatst ontvangen bericht: %2$@ staking No comment provided by engineer. + + subscriber + member role + this contact dit contact diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 17c56468d0..879c32f9f7 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -197,6 +197,18 @@ %d miesięcy time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1688,11 +1700,6 @@ new chat action Zmień tryb blokady authentication reason - - Change member role? - Zmienić rolę członka? - No comment provided by engineer. - Change passcode Zmień pin @@ -1713,6 +1720,10 @@ new chat action Zmień rolę No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Zmień tryb samozniszczenia @@ -2278,14 +2289,6 @@ To jest twój jednorazowy link! Łączenie z komputerem No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Połączenie @@ -2301,6 +2304,10 @@ To jest twój jednorazowy link! Połączenie zablokowane No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Błąd połączenia @@ -3080,7 +3087,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@. No comment provided by engineer. @@ -3090,7 +3097,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@. No comment provided by engineer. @@ -3636,7 +3643,7 @@ chat item action Error Błąd - conn error description + No comment provided by engineer. Error aborting address change @@ -3933,6 +3940,10 @@ chat item action Błąd zapisu profilu grupy No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Błąd zapisu pinu @@ -4066,6 +4077,7 @@ chat item action Error: %@ Błąd: %@ alert message +conn error description file error text snd error text @@ -5279,6 +5291,14 @@ To jest twój link do grupy %@! Mniejszy ruch w sieciach komórkowych. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5457,21 +5477,6 @@ To jest twój link do grupy %@! Raporty członków chat feature - - Member role will be changed to "%@". All chat members will be notified. - Rola członka zostanie zmieniona na "%@". Wszyscy członkowie czatu zostaną o tym poinformowani. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Rola członka grupy zostanie zmieniona na "%@". Wszyscy członkowie grupy zostaną powiadomieni. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Rola członka zostanie zmieniona na "%@". Członek otrzyma nowe zaproszenie. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Członek zostanie usunięty z czatu – nie można tego cofnąć! @@ -5826,6 +5831,10 @@ To jest twój link do grupy %@! Nazwa swipe action + + Name not found + No comment provided by engineer. + Network & servers Sieć i serwery @@ -6147,6 +6156,10 @@ The most secure encryption. Brak serwerów aby otrzymać wiadomości. servers error + + No servers to resolve names. + servers warning + No servers to send files. Brak serwerów do wysyłania plików. @@ -6162,6 +6175,10 @@ The most secure encryption. Brak nieprzeczytanych czatów No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nikt nie śledził twoich rozmów. Nikt nie rysował mapy miejsc, w których byłeś. Prywatność nigdy nie była funkcją - była sposobem na życie. @@ -6171,6 +6188,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Nie chodzi o lepszy zamek w drzwiach kogoś innego. Nie chodzi o milszego właściciela, który szanuje twoją prywatność, ale nadal prowadzi rejestr wszystkich odwiedzających. Nie jesteś gościem. Jesteś w domu. Żaden król nie może do niego wejść - jesteś suwerenem. @@ -6575,8 +6596,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6766,10 +6787,6 @@ Błąd: %@ Spróbuj wyłączyć, a następnie ponownie włączyć powiadomienia. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Poczekaj, aż moderatorzy grupy rozpatrzą Twoją prośbę o dołączenie do grupy. @@ -7474,6 +7491,10 @@ swipe action Zresetuj do motywu użytkownika No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Uruchom ponownie aplikację, aby utworzyć nowy profil czatu @@ -7554,6 +7575,22 @@ swipe action Rola No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Uruchom czat @@ -8089,6 +8126,10 @@ chat item action Serwer No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Serwer został dodany do operatora %@. @@ -8517,6 +8558,18 @@ chat item action Linki SimpleX są niedozwolone No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Zaproszenie jednorazowe SimpleX @@ -8973,6 +9026,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. Adres będzie krótki, a Twój profil zostanie udostępniony za pośrednictwem adresu. @@ -9155,6 +9224,10 @@ your contacts and groups. Można je nadpisać w ustawieniach kontaktu. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną. @@ -9332,6 +9405,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Aby nagrać wiadomość głosową należy udzielić zgody na użycie Mikrofonu. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Aby ujawnić Twój ukryty profil, wprowadź pełne hasło w pole wyszukiwania na stronie **Twoich profili czatu**. @@ -9455,6 +9532,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Niedostarczone wiadomości @@ -9552,19 +9633,11 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Nieprzeczytane swipe action - - Unsupported channel name - alert title - Unsupported connection link Nieobsługiwane łącze połączenia conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9818,6 +9891,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Zweryfikuj kod z komputera @@ -9843,6 +9920,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Zweryfikuj hasło bazy danych No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Zweryfikuj hasło @@ -10481,6 +10562,10 @@ Powtórzyć prośbę połączenia? Twój adres SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Twój kontakt biznesowy @@ -10956,6 +11041,10 @@ marked deleted chat item preview text kontakt powinien zaakceptować… No comment provided by engineer. + + contributor + member role + creator twórca @@ -11601,6 +11690,10 @@ ostatnia otrzymana wiadomość: %2$@ strajk No comment provided by engineer. + + subscriber + member role + this contact ten kontakt diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 4574aa5ec1..70872257a6 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -197,6 +197,18 @@ %d мес time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed %d релеев с ошибками @@ -1720,11 +1732,6 @@ new chat action Изменить режим блокировки authentication reason - - Change member role? - Поменять роль члена группы? - No comment provided by engineer. - Change passcode Изменить код доступа @@ -1745,6 +1752,10 @@ new chat action Поменять роль No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Изменить режим самоуничтожения @@ -2335,14 +2346,6 @@ This is your own one-time link! Подключение к компьютеру No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Соединение @@ -2358,6 +2361,10 @@ This is your own one-time link! Соединение заблокировано No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Ошибка соединения @@ -3147,7 +3154,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. Адрес сервера назначения %@ несовместим с настройками пересылающего сервера %@. No comment provided by engineer. @@ -3157,7 +3164,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. Версия сервера назначения %@ несовместима с пересылающим сервером %@. No comment provided by engineer. @@ -3714,7 +3721,7 @@ chat item action Error Ошибка - conn error description + No comment provided by engineer. Error aborting address change @@ -4014,6 +4021,10 @@ chat item action Ошибка при сохранении профиля группы No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Ошибка сохранения кода @@ -4148,6 +4159,7 @@ chat item action Error: %@ Ошибка: %@ alert message +conn error description file error text snd error text @@ -5370,6 +5382,14 @@ This is your link for group %@! Меньше трафик в мобильных сетях. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you Дайте собеседнику Вашу ссылку @@ -5550,21 +5570,6 @@ This is your link for group %@! Сообщения о нарушениях chat feature - - Member role will be changed to "%@". All chat members will be notified. - Роль участника будет изменена на "%@". Все участники разговора получат уведомление. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Роль члена будет изменена на "%@". Все члены группы получат уведомление. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Роль члена будет изменена на "%@". Будет отправлено новое приглашение. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Член будет удалён из разговора - это действие нельзя отменить! @@ -5924,6 +5929,10 @@ This is your link for group %@! Имя swipe action + + Name not found + No comment provided by engineer. + Network & servers Сеть и серверы @@ -6256,6 +6265,10 @@ The most secure encryption. Нет серверов для приёма сообщений. servers error + + No servers to resolve names. + servers warning + No servers to send files. Нет серверов для отправки файлов. @@ -6271,6 +6284,10 @@ The most secure encryption. Нет непрочитанных чатов No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Никто не отслеживал ваши разговоры. Никто не составлял карту ваших перемещений. Конфиденциальность не была функцией - это был образ жизни. @@ -6281,6 +6298,10 @@ The most secure encryption. Некоммерческое управление No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Не более надёжный замок на чужой двери. Не более вежливый хозяин, который уважает вашу частную жизнь, но всё равно ведёт учёт всех посетителей. Вы не гость. Вы у себя дома. Ни один король не войдёт в ваш дом - вы суверенны. @@ -6699,9 +6720,8 @@ alert button Владелец No comment provided by engineer. - - Owners - Владельцы + + Owners & contributors No comment provided by engineer. @@ -6893,10 +6913,6 @@ Error: %@ Попробуйте выключить и снова включить уведомления. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление. @@ -7617,6 +7633,10 @@ swipe action Сбросить на тему пользователя No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Перезапустите приложение, чтобы создать новый профиль @@ -7697,6 +7717,22 @@ swipe action Роль No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Запустить chat @@ -8241,6 +8277,10 @@ chat item action Сервер No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Сервер добавлен к оператору %@. @@ -8677,6 +8717,18 @@ chat item action Ссылки SimpleX не разрешены No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX одноразовая ссылка @@ -9153,6 +9205,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. Адрес будет коротким, и Ваш профиль будет добавлен в адрес. @@ -9339,6 +9407,10 @@ your contacts and groups. Они могут быть изменены в настройках контактов и групп. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Это действие нельзя отменить - все полученные и отправленные файлы будут удалены. Изображения останутся в низком разрешении. @@ -9519,6 +9591,10 @@ You will be prompted to complete authentication before this feature is enabled.< Для записи голосового сообщения, пожалуйста разрешите доступ к микрофону. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**. @@ -9644,6 +9720,10 @@ You will be prompted to complete authentication before this feature is enabled.< Разблокировать подписчика для всех? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Недоставленные сообщения @@ -9741,19 +9821,11 @@ To connect, please ask your contact to create another connection link and check Не прочитано swipe action - - Unsupported channel name - alert title - Unsupported connection link Ссылка не поддерживается conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -10012,6 +10084,10 @@ To connect, please ask your contact to create another connection link and check Проверить relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Сверьте код с компьютером @@ -10037,6 +10113,10 @@ To connect, please ask your contact to create another connection link and check Проверка пароля базы данных No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Проверить пароль @@ -10686,6 +10766,10 @@ Repeat connection request? Ваш адрес SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Ваш бизнес-контакт @@ -11173,6 +11257,10 @@ marked deleted chat item preview text контакт должен принять… No comment provided by engineer. + + contributor + member role + creator создатель @@ -11825,6 +11913,10 @@ last received msg: %2$@ зачеркнуть No comment provided by engineer. + + subscriber + member role + this contact этот контакт diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index c6a57fb777..311a52159b 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -179,6 +179,18 @@ %d เดือน time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1534,11 +1546,6 @@ new chat action เปลี่ยนโหมดล็อค authentication reason - - Change member role? - เปลี่ยนบทบาทของสมาชิก? - No comment provided by engineer. - Change passcode เปลี่ยนรหัสผ่าน @@ -1559,6 +1566,10 @@ new chat action เปลี่ยนบทบาท No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode เปลี่ยนโหมดทําลายตัวเอง @@ -2064,14 +2075,6 @@ This is your own one-time link! Connecting to desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection การเชื่อมต่อ @@ -2085,6 +2088,10 @@ This is your own one-time link! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error การเชื่อมต่อผิดพลาด @@ -2802,7 +2809,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. @@ -2810,7 +2817,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -3312,7 +3319,7 @@ chat item action Error ผิดพลาด - conn error description + No comment provided by engineer. Error aborting address change @@ -3581,6 +3588,10 @@ chat item action เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม No comment provided by engineer. + + Error saving name + alert title + Error saving passcode เกิดข้อผิดพลาดในการบันทึกรหัสผ่าน @@ -3704,6 +3715,7 @@ chat item action Error: %@ ข้อผิดพลาด: % @ alert message +conn error description file error text snd error text @@ -4808,6 +4820,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -4970,20 +4990,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกจะได้รับคำเชิญใหม่ - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5301,6 +5307,10 @@ This is your link for group %@! ชื่อ swipe action + + Name not found + No comment provided by engineer. + Network & servers เครือข่ายและเซิร์ฟเวอร์ @@ -5589,6 +5599,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5601,6 +5615,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -5609,6 +5627,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -5969,8 +5991,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6146,10 +6168,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6784,6 +6802,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่ @@ -6859,6 +6881,22 @@ swipe action บทบาท No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat เรียกใช้แชท @@ -7346,6 +7384,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7727,6 +7769,18 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation คำเชิญ SimpleX แบบครั้งเดียว @@ -8147,6 +8201,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8310,6 +8380,10 @@ your contacts and groups. They can be overridden in contact and group settings. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่ @@ -8469,6 +8543,10 @@ You will be prompted to complete authentication before this feature is enabled.< ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้า **โปรไฟล์แชทของคุณ** @@ -8576,6 +8654,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8669,18 +8751,10 @@ To connect, please ask your contact to create another connection link and check เปลี่ยนเป็นยังไม่ได้อ่าน swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -8901,6 +8975,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop No comment provided by engineer. @@ -8922,6 +9000,10 @@ To connect, please ask your contact to create another connection link and check Verify database passphrase No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase No comment provided by engineer. @@ -9503,6 +9585,10 @@ Repeat connection request? ที่อยู่ SimpleX ของคุณ No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -9946,6 +10032,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator ผู้สร้าง @@ -10547,6 +10637,10 @@ last received msg: %2$@ ตี No comment provided by engineer. + + subscriber + member role + this contact ผู้ติดต่อนี้ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index f92ebbde0e..27095ac5b2 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -197,6 +197,18 @@ %d ay time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed %d aktarıcı başarısız oldu @@ -1694,11 +1706,6 @@ new chat action Kilit modunu değiştir authentication reason - - Change member role? - Üye rolünü değiştir? - No comment provided by engineer. - Change passcode Şifreyi değiştir @@ -1719,6 +1726,10 @@ new chat action Rolü değiştir No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Kendini yok etme modunu değiştir @@ -2284,14 +2295,6 @@ Bu senin kendi tek kullanımlık bağlantın! Bilgisayara bağlanıyor No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Bağlantı @@ -2307,6 +2310,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı engellendi No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Bağlantı hatası @@ -3083,7 +3090,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. Hedef sunucu adresi %@, yönlendirme sunucusu %@ ayarlarıyla uyumlu değil. No comment provided by engineer. @@ -3093,7 +3100,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. Hedef sunucu %@ sürümü, yönlendirme sunucusu %@ ile uyumlu değil. No comment provided by engineer. @@ -3639,7 +3646,7 @@ chat item action Error Hata - conn error description + No comment provided by engineer. Error aborting address change @@ -3935,6 +3942,10 @@ chat item action Grup profili kaydedilirken sorun oluştu No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Parola kaydedilirken sorun oluştu @@ -4068,6 +4079,7 @@ chat item action Error: %@ Hata: %@ alert message +conn error description file error text snd error text @@ -5273,6 +5285,14 @@ Bu senin grup için bağlantın %@! Mobil ağlarda daha az trafik. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5449,21 +5469,6 @@ Bu senin grup için bağlantın %@! Üye raporları chat feature - - Member role will be changed to "%@". All chat members will be notified. - Üye rolü "%@" olarak değiştirilecektir. Tüm sohbet üyeleri bilgilendirilecektir. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Üye rolü "%@" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Üye rolü "%@" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Üye sohbetten kaldırılacak - bu geri alınamaz! @@ -5818,6 +5823,10 @@ Bu senin grup için bağlantın %@! İsim swipe action + + Name not found + No comment provided by engineer. + Network & servers Ağ & sunucular @@ -6139,6 +6148,10 @@ The most secure encryption. Mesaj almak için hiç sunucu yok. servers error + + No servers to resolve names. + servers warning + No servers to send files. Dosya göndermek için hiç sunucu yok. @@ -6154,6 +6167,10 @@ The most secure encryption. Okunmamış sohbet yok No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -6162,6 +6179,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6565,8 +6586,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6756,10 +6777,6 @@ Hata: %@ Lütfen bildirimleri devre dışı bırakmayı ve yeniden etkinleştirmeyi deneyin. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Lütfen grup moderatörlerinin gruba katılma isteğinizi incelemesini bekleyin. @@ -7463,6 +7480,10 @@ swipe action Kullanıcı temasına sıfırla No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Yeni bir sohbet profili oluşturmak için uygulamayı yeniden başlatın @@ -7543,6 +7564,22 @@ swipe action Rol No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Sohbeti çalıştır @@ -8073,6 +8110,10 @@ chat item action Sunucu No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Sunucu operatör %@'ya eklendi. @@ -8501,6 +8542,18 @@ chat item action SimpleX bağlantılarına izin verilmiyor No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX tek kullanımlık davet @@ -8957,6 +9010,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. Adres kısa olacak ve profiliniz bu adres üzerinden paylaşılacaktır. @@ -9136,6 +9205,10 @@ your contacts and groups. Bunlar kişi ve grup ayarlarında geçersiz kılınabilir. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Bu işlem geri alınamaz - alınan ve gönderilen tüm dosyalar ve medya silinecektir. Düşük çözünürlüklü resimler kalacaktır. @@ -9313,6 +9386,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Sesli mesaj kaydetmek için lütfen Mikrofon kullanım izni verin. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Gizli profilinizi ortaya çıkarmak için **Sohbet profilleriniz** sayfasındaki arama alanına tam bir şifre girin. @@ -9435,6 +9512,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Teslim edilmemiş mesajlar @@ -9532,19 +9613,11 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Okunmamış swipe action - - Unsupported channel name - alert title - Unsupported connection link Desteklenmeyen bağlantı bağlantısı conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9798,6 +9871,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Bilgisayarla kodu doğrula @@ -9823,6 +9900,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Veritabanı parolasını doğrulayın No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Parolayı doğrula @@ -10457,6 +10538,10 @@ Bağlantı isteği tekrarlansın mı? SimpleX adresin No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact İş bağlantınız @@ -10931,6 +11016,10 @@ marked deleted chat item preview text kişi kabul etmeli… No comment provided by engineer. + + contributor + member role + creator oluşturan @@ -11574,6 +11663,10 @@ son alınan msj: %2$@ çizik No comment provided by engineer. + + subscriber + member role + this contact Bu kişi diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index a356858ecd..1da658611d 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -197,6 +197,18 @@ %d місяців time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1680,11 +1692,6 @@ new chat action Зміна режиму блокування authentication reason - - Change member role? - Змінити роль учасника? - No comment provided by engineer. - Change passcode Змінити код доступу @@ -1705,6 +1712,10 @@ new chat action Змінити роль No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Змінити режим самознищення @@ -2270,14 +2281,6 @@ This is your own one-time link! Підключення до ПК No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Підключення @@ -2293,6 +2296,10 @@ This is your own one-time link! Підключення заблоковано No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Помилка підключення @@ -3067,7 +3074,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. Адреса сервера призначення %@ несумісна з налаштуваннями сервера пересилання %@. No comment provided by engineer. @@ -3077,7 +3084,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. Версія сервера призначення %@ несумісна з версією сервера переадресації %@. No comment provided by engineer. @@ -3623,7 +3630,7 @@ chat item action Error Помилка - conn error description + No comment provided by engineer. Error aborting address change @@ -3919,6 +3926,10 @@ chat item action Помилка збереження профілю групи No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Помилка збереження пароля @@ -4051,6 +4062,7 @@ chat item action Error: %@ Помилка: %@ alert message +conn error description file error text snd error text @@ -5255,6 +5267,14 @@ This is your link for group %@! Менше трафіку в мобільних мережах. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5429,21 +5449,6 @@ This is your link for group %@! Повідомлення учасників chat feature - - Member role will be changed to "%@". All chat members will be notified. - Роль учасника буде змінено на "%@". Усі учасники чату отримають сповіщення. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Роль учасника буде змінено на "%@". Учасник отримає нове запрошення. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Учасника буде видалено з чату – це неможливо скасувати! @@ -5798,6 +5803,10 @@ This is your link for group %@! Ім'я swipe action + + Name not found + No comment provided by engineer. + Network & servers Мережа та сервери @@ -6119,6 +6128,10 @@ The most secure encryption. Немає серверів для отримання повідомлень. servers error + + No servers to resolve names. + servers warning + No servers to send files. Немає серверів для надсилання файлів. @@ -6134,6 +6147,10 @@ The most secure encryption. Немає непрочитаних чатів No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -6142,6 +6159,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6540,8 +6561,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6731,10 +6752,6 @@ Error: %@ Будь ласка, спробуйте вимкнути та знову увімкнути сповіщення. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Будь ласка, зачекайте, поки модератори групи розглянуть ваш запит на приєднання до групи. @@ -7437,6 +7454,10 @@ swipe action Повернутися до теми користувача No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Перезапустіть програму, щоб створити новий профіль чату @@ -7517,6 +7538,22 @@ swipe action Роль No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Запустити чат @@ -8047,6 +8084,10 @@ chat item action Сервер No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Сервер додано до оператора %@. @@ -8475,6 +8516,18 @@ chat item action Посилання SimpleX заборонені No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Одноразове запрошення SimpleX @@ -8930,6 +8983,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. Адреса буде короткою, і ваш профіль буде доступний за цією адресою. @@ -9109,6 +9178,10 @@ your contacts and groups. Їх можна перевизначити в налаштуваннях контактів і груп. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Цю дію неможливо скасувати - всі отримані та надіслані файли і медіа будуть видалені. Зображення з низькою роздільною здатністю залишаться. @@ -9285,6 +9358,10 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**. @@ -9406,6 +9483,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Недоставлені повідомлення @@ -9503,19 +9584,11 @@ To connect, please ask your contact to create another connection link and check Непрочитане swipe action - - Unsupported channel name - alert title - Unsupported connection link Несумісне посилання для підключення conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9769,6 +9842,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Перевірте код на робочому столі @@ -9794,6 +9871,10 @@ To connect, please ask your contact to create another connection link and check Перевірте пароль до бази даних No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Підтвердіть парольну фразу @@ -10428,6 +10509,10 @@ Repeat connection request? Ваша адреса SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Ваш діловий контакт @@ -10902,6 +10987,10 @@ marked deleted chat item preview text контакт повинен прийняти… No comment provided by engineer. + + contributor + member role + creator творець @@ -11543,6 +11632,10 @@ last received msg: %2$@ закреслено No comment provided by engineer. + + subscriber + member role + this contact цей контакт diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index d2558bf6d8..ed361099e1 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -197,6 +197,18 @@ %d 月 time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -1688,11 +1700,6 @@ new chat action 更改锁定模式 authentication reason - - Change member role? - 更改成员角色? - No comment provided by engineer. - Change passcode 更改密码 @@ -1713,6 +1720,10 @@ new chat action 改变角色 No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode 更改自毁模式 @@ -2278,14 +2289,6 @@ This is your own one-time link! 正连接到桌面 No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection 连接 @@ -2301,6 +2304,10 @@ This is your own one-time link! 连接被阻止 No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error 连接错误 @@ -3077,7 +3084,7 @@ alert button No comment provided by engineer. - Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. 目标服务器地址 %@ 与转发服务器 %@ 设置不兼容。 No comment provided by engineer. @@ -3087,7 +3094,7 @@ alert button snd error text - Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. 目标服务器版本 %@ 与转发服务器 %@ 不兼容。 No comment provided by engineer. @@ -3633,7 +3640,7 @@ chat item action Error 错误 - conn error description + No comment provided by engineer. Error aborting address change @@ -3929,6 +3936,10 @@ chat item action 保存群组资料错误 No comment provided by engineer. + + Error saving name + alert title + Error saving passcode 保存密码错误 @@ -4062,6 +4073,7 @@ chat item action Error: %@ 错误: %@ alert message +conn error description file error text snd error text @@ -5274,6 +5286,14 @@ This is your link for group %@! 消耗更少的移动网络数据。 No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5451,21 +5471,6 @@ This is your link for group %@! 成员举报 chat feature - - Member role will be changed to "%@". All chat members will be notified. - 将变更成员角色为“%@”。所有成员都会收到通知。 - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - 成员角色将更改为 "%@"。所有群成员将收到通知。 - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - 成员角色将更改为 "%@"。该成员将收到一份新的邀请。 - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! 将从聊天中删除成员 - 此操作无法撤销! @@ -5820,6 +5825,10 @@ This is your link for group %@! 名称 swipe action + + Name not found + No comment provided by engineer. + Network & servers 网络和服务器 @@ -6141,6 +6150,10 @@ The most secure encryption. 无消息接收服务器。 servers error + + No servers to resolve names. + servers warning + No servers to send files. 无文件发送服务器。 @@ -6156,6 +6169,10 @@ The most secure encryption. 没有未读聊天 No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. 没有人追踪你的谈话内容。没有人绘制你去过的地方的地图。隐私从来都不是一项功能--而是一种生活方式。 @@ -6165,6 +6182,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. 别人家的门锁再好也比不上这里。房东再好也比不上这里,他既尊重你的隐私,又保留着所有访客的记录。你不是客人,你是家。没有国王能闯入--你是主人。 @@ -6569,8 +6590,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6760,10 +6781,6 @@ Error: %@ 请尝试禁用并重新启用通知。 token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. 请等待群的协管审核你加入该群的请求。 @@ -7467,6 +7484,10 @@ swipe action 重置为用户主题 No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile 重新启动应用程序以创建新的聊天资料 @@ -7546,6 +7567,22 @@ swipe action 角色 No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat 运行聊天 @@ -8081,6 +8118,10 @@ chat item action 服务器 No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. 服务器已添加到运营方 %@。 @@ -8509,6 +8550,18 @@ chat item action 不允许SimpleX 链接 No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX 一次性邀请 @@ -8964,6 +9017,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. 地址不会长,将通过该简短地址分享个人资料。 @@ -9144,6 +9213,10 @@ your contacts and groups. 可以在联系人和群组设置中覆盖它们。 No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. 此操作无法撤消——所有接收和发送的文件和媒体都将被删除。 低分辨率图片将保留。 @@ -9321,6 +9394,10 @@ You will be prompted to complete authentication before this feature is enabled.< 请授权使用麦克风以录制语音消息。 No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. 要显示您的隐藏的个人资料,请在**您的聊天个人资料**页面的搜索字段中输入完整密码。 @@ -9443,6 +9520,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages 未送达的消息 @@ -9540,19 +9621,11 @@ To connect, please ask your contact to create another connection link and check 未读 swipe action - - Unsupported channel name - alert title - Unsupported connection link 不支持的连接链接 conn error description - - Unsupported contact name - alert title - Unverified badge badge alert title @@ -9806,6 +9879,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop 用桌面端验证代码 @@ -9831,6 +9908,10 @@ To connect, please ask your contact to create another connection link and check 验证数据库密码短语 No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase 验证密码短语 @@ -10468,6 +10549,10 @@ Repeat connection request? 您的 SimpleX 地址 No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact 你的企业联系人 @@ -10940,6 +11025,10 @@ marked deleted chat item preview text 联系人应当接受… No comment provided by engineer. + + contributor + member role + creator 创建者 @@ -11583,6 +11672,10 @@ last received msg: %2$@ 删去 No comment provided by engineer. + + subscriber + member role + this contact 这个联系人 From 6bb1da9e8dfa5237b303f8e75518e446660ecfb6 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:32:29 +0000 Subject: [PATCH 17/21] core: request roster (#7121) --- .../2026-06-22-roster-catchup-subscribers.md | 83 +++++++++++++ simplex-chat.cabal | 2 + src/Simplex/Chat/Library/Commands.hs | 56 +++++---- src/Simplex/Chat/Library/Internal.hs | 49 +++++--- src/Simplex/Chat/Library/Subscriber.hs | 100 +++++++++++---- src/Simplex/Chat/Protocol.hs | 7 ++ src/Simplex/Chat/Store/Groups.hs | 65 ++++++++-- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20260629_roster_catchup.hs | 41 ++++++ .../Store/Postgres/Migrations/chat_schema.sql | 7 +- src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20260629_roster_catchup.hs | 40 ++++++ .../SQLite/Migrations/chat_query_plans.txt | 66 +++++++++- .../Store/SQLite/Migrations/chat_schema.sql | 5 +- tests/ChatTests/Groups.hs | 117 ++++++++++++++++++ 15 files changed, 562 insertions(+), 84 deletions(-) create mode 100644 plans/2026-06-22-roster-catchup-subscribers.md create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20260629_roster_catchup.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20260629_roster_catchup.hs diff --git a/plans/2026-06-22-roster-catchup-subscribers.md b/plans/2026-06-22-roster-catchup-subscribers.md new file mode 100644 index 0000000000..fd1979cda0 --- /dev/null +++ b/plans/2026-06-22-roster-catchup-subscribers.md @@ -0,0 +1,83 @@ +# Roster catch-up for channel subscribers + +Continues the public-groups roster work (privileged roster, member keys, `VersionRoster` monotonic gate, task-047's `Maybe VersionRoster` on `XGrpMemRole`/`XGrpMemDel`). + +## Problem + +A channel subscriber learns the full roster only on join and on resume (`serveRoster`, Subscriber.hs:1174/1269), then tracks it via forwarded `XGrpMemRole`/`XGrpMemDel` deltas, each carrying an incrementing `VersionRoster`. The gate (`applyAtRosterVersion`, Subscriber.hs:3248) accepts any delta at `v >= cur` and advances to `v`. + +If the subscriber misses intermediate deltas and then receives one at a version more than one above its known version, it advances to `v` and applies that one change — but the roster changes carried by the skipped versions (other members' roles, removals, keys) are lost until the next resume. The subscriber can then reject a freshly-promoted member's signed action (`RGEMsgBadSignature`) for a key it never learned. + +Catch-up: on detecting a gap, the subscriber asks the relay that forwarded the delta to re-serve the full roster, which carries the complete set and keys. + +## Design + +Four pieces. No schema change, no new store function. + +### 1. Owner sends the roster before the delta (the enabler) + +Today the owner sends `XGrpMemRole`/`XGrpMemDel` first, then `broadcastRoster` (Commands.hs:2754/2885). The relay forwards the delta to subscribers *before* its own roster-blob transfer completes, so for the whole transfer its stored `roster_blob` lags its `roster_version`. A catch-up request in that window serves a stale blob the subscriber rejects (`notBelowRoster`) — which is the common case, not an edge. + +Fix: in `APIMembersRole` / `APIRemoveMembers`, reorder to **apply the change to the owner's members → `broadcastRoster v` → send the delta**. This restructures `changeRoleCurrentMems` / `deleteMemsSend` to separate "apply to owner DB" from "send delta to relays" (the roster is still built after the change, so it reflects the new roles / excludes the removed member). + +Effect, relying on FIFO order of the owner→relay connection (a guarantee the roster design already assumes): the relay applies and stores the blob at `v` (`setGroupLiveRoster`) before it processes and forwards the delta at `v`. So a relay's `roster_version` always reflects its stored blob, and any request triggered by a forwarded delta finds a current blob. The now-redundant delta hits the existing no-op suppression (`fromRole == memRole`, Subscriber.hs:3298, added in task-047) and is still forwarded to subscribers — no new relay logic, subscribers still see the delta. + +Behavioral change to core delivery (all relays/subscribers) → its own commit. Implementation must confirm FIFO holds owner→relay (the test will expose a violation). Residual: a failed roster send (rare; already `catchAllErrors`) leaves that relay's gate briefly above its blob until the next change — heals on resume. + +### 2. New event `XGrpRosterRequest VersionRoster` (Protocol.hs) + +A directed subscriber→relay control message carrying the subscriber's pre-gap version, so the relay can skip serving when it holds nothing newer. + +```haskell +XGrpRosterRequest :: VersionRoster -> ChatMsgEvent 'Json +``` + +Wire plumbing (standard single-`VersionRoster` `'Json` event): GADT constructor; `CMEventTag` `XGrpRosterRequest_`; `strEncode` `"x.grp.roster.request"`; `strP` case; `toCMEventTag`; `appJsonToCM` (`XGrpRosterRequest <$> p "version"`); `chatToAppMessage` (`o ["version" .= v]`). NOT in `isForwardedGroupMsg` (point-to-point). NOT in `requiresSignature` (subscribers/observers have no key). + +### 3. Subscriber detects the gap and requests (Subscriber.hs, `applyAtRosterVersion`) + +`applyAtRosterVersion` already reads `cur`, compares `v >= cur`, advances the gate — the one path shared by `xGrpMemRole` and `xGrpMemDel`. In its accepting branch, when the receiver is a subscriber (`not (isUserGrpFwdRelay gInfo)`) and `cur = Just c` with `v > c + 1`, send `XGrpRosterRequest c` to the relay that forwarded the delta, then run the action unchanged (the delta is still applied; the roster heals `c+1 .. v-1`). + +To target the forwarding relay, thread it as `Maybe GroupMember` through `xGrpMemRole` / `xGrpMemDel` / `applyAtRosterVersion`: `Just m` on the forwarded path (`m` in scope in `processForwardedMsg`, Subscriber.hs:3804/3806), `Nothing` on the direct path (1092/1096; the receiver there is a relay and never requests). Send via `sendGroupMessage' user gInfo [relay] (XGrpRosterRequest c)`. + +One forwarding relay only, not all relays: avoids N² requests under concurrent multi-relay gaps, and the owner-signed roster needs no cross-relay verification. The gate advances to `v` on the first gap in a batch, so later `+1` deltas aren't gaps — normally one request per batch. + +### 4. Relay serves on request (Subscriber.hs, `processEvent` + new `xGrpRosterRequest`) + +The request arrives on the direct member connection, dispatched in `processEvent` (alongside other direct group events, ~Subscriber.hs:1103): + +```haskell +XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest gInfo' m'' reqVer +``` + +```haskell +xGrpRosterRequest :: GroupInfo -> GroupMember -> VersionRoster -> CM () +xGrpRosterRequest gInfo m reqVer = + when (isUserGrpFwdRelay gInfo) $ do + cur <- withStore' $ \db -> getGroupRosterVersion db gInfo + when (maybe True (> reqVer) cur) $ serveRoster user gInfo m +``` + +Reuses `serveRoster` unchanged (signed header + inline blob chunks to the one requester — the join/resume path). The version check serves only when the relay holds something newer than the requester, rate-limiting same-version spam. With piece 1, the served blob is current, so the subscriber accepts it (`notBelowRoster`) and `rosterCompletion` heals the skipped set and keys. `serveRoster` is a no-op without a stored roster. + +## Files touched + +- `src/Simplex/Chat/Library/Commands.hs` — reorder owner sends in `APIMembersRole`/`APIRemoveMembers` (restructure `changeRoleCurrentMems`/`deleteMemsSend`). +- `src/Simplex/Chat/Protocol.hs` — new event, end to end. +- `src/Simplex/Chat/Library/Subscriber.hs` — gap detection + request in `applyAtRosterVersion`; thread forwarding relay through `xGrpMemRole`/`xGrpMemDel`; new `xGrpRosterRequest`; one `processEvent` dispatch line. +- No schema change, no new store fn (reuses `getGroupRosterVersion`, `getGroupRelayMembers`, `getGroupRoster`, `serveRoster`). +- `tests/ChatTests/` — one channel catch-up test. + +## Tests + +A `channels` test asserting a subscriber that observes a version gap recovers the skipped set. The hard part is staging a deterministic gap (FIFO forwarding doesn't drop deltas). Candidate: two relays where an intermediate delta reaches only one path, so the subscriber receives a later delta at a jumped version; assert it emits exactly one `XGrpRosterRequest`, the relay re-serves, and a member promoted in the skipped interval is afterwards known (its signed action accepted, no `RGEMsgBadSignature`). Confirm the gap is reliably reproducible before finalizing; fall back to asserting "a forwarded delta at a jumped version triggers one request + re-serve" if the two-relay setup is flaky. Iterate with `-m "channels"`. + +## Commits + +1. Reorder owner sends (roster before delta) — relay blob is current before it forwards deltas. +2. Protocol: add `XGrpRosterRequest`. +3. Relay: `xGrpRosterRequest` + `processEvent` dispatch. +4. Subscriber: gap detection + request to the forwarding relay. +5. Test + schema/plan regen. + +Build and run `-m "channels"` after each. diff --git a/simplex-chat.cabal b/simplex-chat.cabal index cb36e989da..e84e50bd0a 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -146,6 +146,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name + Simplex.Chat.Store.Postgres.Migrations.M20260629_roster_catchup else exposed-modules: Simplex.Chat.Archive @@ -309,6 +310,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name + Simplex.Chat.Store.SQLite.Migrations.M20260629_roster_catchup other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 836f4e72fd..772e10dfb0 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2795,34 +2795,35 @@ processChatCommand cxt nm = \case -- TODO [relays] possible optimization is to read only required members + relays g@(Group gInfo members) <- withFastStore $ \db -> getGroup db cxt user groupId when (selfSelected gInfo) $ throwCmdError "can't change role for self" - let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending, anyPrivilegedTarget, finalPrivilegedCount) = selectMembers members + let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending, anyPrivilegedTarget, anyRosterChange, finalPrivilegedCount) = selectMembers members when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $ throwCmdError "can't change role of multiple members when admins selected, or new role is admin" when anyPending $ throwCmdError "can't change role of members pending approval" assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole]) - -- in relay groups the roster has a single signer, so only the owner may change moderator/admin roles + -- in relay groups the roster has a single signer, so only the owner may change member/moderator/admin roles when (useRelays' gInfo && (isRosterRole newRole || anyPrivilegedTarget) && memberRole' (membership gInfo) /= GROwner) $ throwCmdError "only the group owner can change moderator and admin roles" when (useRelays' gInfo && isRosterRole newRole && finalPrivilegedCount > maxGroupRosterSize) $ throwCmdError $ "the number of members, moderators and admins would exceed the limit of " <> show maxGroupRosterSize (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems - let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && (isRosterRole newRole || anyPrivilegedTarget) - rosterVer <- if doBumpRoster then Just <$> reserveRosterVersion gInfo else pure Nothing + let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyRosterChange + -- roster (with the change projected in) before the delta, so a relay stores the blob at this version before forwarding the delta + rosterVer <- if doBumpRoster then Just <$> broadcastRoster user gInfo (RDRoleChanged newRole currentMems) else pure Nothing (errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g rosterVer currentMems - forM_ rosterVer $ \v -> broadcastRoster user gInfo v `catchAllErrors` eToView unless (null acis) $ toView $ CEvtNewChatItems user acis let errs = errs1 <> errs2 unless (null errs) $ toView $ CEvtChatErrors errs pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole, msgSigned} -- same order is not guaranteed where selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds - -- anyPrivilegedTarget: a target currently moderator/admin; finalPrivilegedCount: - -- moderators + admins after the change (targets take newRole, others keep their role). - selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool, Bool, Int) - selectMembers = foldr' addMember ([], [], [], GRObserver, False, False, False, 0) + -- anyPrivilegedTarget: a target currently member/moderator/admin (gates the owner-only check); anyRosterChange: + -- a current member's role change that alters the roster blob - the only case that bumps the version, since a + -- bump with no delta reads as a gap to subscribers; finalPrivilegedCount: moderators + admins after the change. + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool, Bool, Bool, Int) + selectMembers = foldr' addMember ([], [], [], GRObserver, False, False, False, False, 0) where - addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, privCount) + addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, anyRosterChange, privCount) | groupMemberId `elem` memberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin @@ -2830,10 +2831,11 @@ processChatCommand cxt nm = \case anyPrivTarget' = anyPrivTarget || isRosterRole memberRole privCount' = if isRosterRole newRole then privCount + 1 else privCount in if - | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', privCount') - | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', privCount') - | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', privCount') - | otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, if isRosterRole memberRole then privCount + 1 else privCount) + | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRosterChange, privCount') + | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRosterChange, privCount') + -- a current member's role actually changes here; it alters the roster iff the old or new role is on it + | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRosterChange || isRosterRole newRole || isRosterRole memberRole, privCount') + | otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, anyRosterChange, if isRosterRole memberRole then privCount + 1 else privCount) changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember]) changeRoleInvitedMems user gInfo memsToChange = do -- not batched, as we need to send different invitations to different connections anyway @@ -2925,7 +2927,7 @@ processChatCommand cxt nm = \case withGroupLock "removeMembers" groupId $ do -- TODO [relays] possible optimization is to read only required members + relays Group gInfo members <- withFastStore $ \db -> getGroup db cxt user groupId - let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin, anyPrivilegedRemoved) = selectMembers gmIds members + let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin, anyPrivilegedRemoved, anyRosterRemoved) = selectMembers gmIds members gmIds = S.fromList $ L.toList groupMemberIds memCount = length groupMemberIds when (count /= memCount) $ throwChatError CEGroupMemberNotFound @@ -2935,15 +2937,15 @@ processChatCommand cxt nm = \case throwCmdError "only the group owner can remove members, moderators and admins" (errs1, deleted1) <- deleteInvitedMems user invitedMems let recipients = filter memberCurrent members - let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyPrivilegedRemoved - rosterVer <- if doBumpRoster then Just <$> reserveRosterVersion gInfo else pure Nothing + let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyRosterRemoved + -- roster (excluding the removed members) before the delta, so a relay stores the blob at this version before forwarding the delta + rosterVer <- if doBumpRoster then Just <$> broadcastRoster user gInfo (RDRemoved currentMems) else pure Nothing (errs2, deleted2, acis2, signed2) <- deleteMemsSend user gInfo Nothing rosterVer recipients currentMems (errs3, deleted3, acis3, signed3) <- foldM (\acc m -> deletePendingMember acc user gInfo [m] m) ([], [], [], False) pendingApprvMems let moderators = filter (\GroupMember {memberRole} -> memberRole >= GRModerator) members (errs4, deleted4, acis4, signed4) <- foldM (\acc m -> deletePendingMember acc user gInfo (m : moderators) m) ([], [], [], False) pendingRvwMems - forM_ rosterVer $ \v -> broadcastRoster user gInfo v `catchAllErrors` eToView let acis = acis2 <> acis3 <> acis4 errs = errs1 <> errs2 <> errs3 <> errs4 deleted = deleted1 <> deleted2 <> deleted3 <> deleted4 @@ -2958,20 +2960,24 @@ processChatCommand cxt nm = \case unless (null errs) $ toView $ CEvtChatErrors errs pure $ CRUserDeletedMembers user gInfo' deleted withMessages msgSigned -- same order is not guaranteed where - selectMembers :: S.Set GroupMemberId -> [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) - selectMembers gmIds = foldl' addMember (0, [], [], [], [], GRObserver, False, False) + -- anyPrivilegedRemoved: any removed member is member/moderator/admin (gates the owner-only check); anyRosterRemoved: + -- a current roster member is removed - the only case that alters the blob and so bumps the version (pending/ + -- invited members aren't on the roster, and a bump with no delta reads as a gap to subscribers). + selectMembers :: S.Set GroupMemberId -> [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool, Bool) + selectMembers gmIds = foldl' addMember (0, [], [], [], [], GRObserver, False, False, False) where - addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin, anyPrivRemoved) m@GroupMember {groupMemberId, memberStatus, memberRole} + addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin, anyPrivRemoved, anyRosterRemoved) m@GroupMember {groupMemberId, memberStatus, memberRole} | groupMemberId `S.member` gmIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin anyPrivRemoved' = anyPrivRemoved || isRosterRole memberRole n' = n + 1 in case memberStatus of - GSMemInvited -> (n', m : invited, pendingApprv, pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved') - GSMemPendingApproval -> (n', invited, m : pendingApprv, pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved') - GSMemPendingReview -> (n', invited, pendingApprv, m : pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved') - _ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin', anyPrivRemoved') + GSMemInvited -> (n', m : invited, pendingApprv, pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved) + GSMemPendingApproval -> (n', invited, m : pendingApprv, pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved) + GSMemPendingReview -> (n', invited, pendingApprv, m : pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved) + -- removed current member: alters the roster blob iff it currently holds a roster role + _ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved || isRosterRole memberRole) | otherwise = acc deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember]) deleteInvitedMems user memsToDelete = do diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index d34e08401b..62ce525cbd 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1187,8 +1187,8 @@ serveRoster :: User -> GroupInfo -> GroupMember -> CM () serveRoster user gInfo member = when (member `supportsVersion` groupRosterVersion) $ do cxt <- chatStoreCxt - withStore' (\db -> getGroupRoster db gInfo) >>= \case - Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_) -> + withStore' (\db -> getStoredGroupRoster db gInfo) >>= \case + Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_, storedVer_) -> case J.eitherDecodeStrict' signedBody :: Either String (ChatMessage 'Json) of Left e -> logError $ "serveRoster: cannot decode saved roster message: " <> tshow e Right chatMsg@ChatMessage {msgId} -> @@ -1198,6 +1198,10 @@ serveRoster user gInfo member = sendFwdMemberMessage member fwd (VMSigned MSSVerified sm chatMsg) forM_ ((,) <$> msgId <*> blob_) $ \(sid, blob) -> sendInlineBlobChunks user gInfo [member] sid blob + -- record the blob's own stored version as served, not roster_version (the gate): a delta can + -- advance the gate past the stored blob on a failed blob send, and recording the gate would + -- over-claim what this member was actually served, suppressing legitimate catch-up + forM_ storedVer_ $ \v -> withStore' $ \db -> setMemberRosterServedVersion db member v Left e -> logError $ "serveRoster: roster owner not found: " <> tshow e Nothing -> pure () @@ -2285,24 +2289,39 @@ sendGroupMessage' user gInfo members chatMsgEvent = ((Right msg) :| [], _) -> pure msg _ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message" +-- The roster change being broadcast, projected onto the current roster members in broadcastRoster. This lets the +-- roster blob be built (and sent) before the change is applied to the owner's own member records, so the owner +-- never demotes/removes a member locally before the change has been propagated to relays. +data RosterDelta + = RDRoleChanged GroupMemberRole [GroupMember] -- these members now hold this role + | RDRemoved [GroupMember] -- these members are removed from the group + +applyRosterDelta :: RosterDelta -> [GroupMember] -> [GroupMember] +applyRosterDelta delta current = case delta of + RDRoleChanged role changed -> map (\m -> (m :: GroupMember) {memberRole = role}) changed <> without changed + RDRemoved removed -> without removed + where + without ms = let ids = S.fromList (map groupMemberId' ms) in filter ((`S.notMember` ids) . groupMemberId') current + -- TODO [relays] improvement: publish roster_version in link data so the owner can recover the latest version -- TODO after restoring from a stale backup (relays accept only strictly-greater versions) --- Persist the next roster version before sending the events that carry it (so a recipient never advances --- past a version the owner hasn't recorded). The matching blob is broadcast separately, by broadcastRoster, --- after the change is applied to the owner's members - so the served roster excludes demoted/removed members. -reserveRosterVersion :: GroupInfo -> CM VersionRoster -reserveRosterVersion gInfo = do +-- Reserve and persist the next roster version (committed before the events that carry it, so a recipient never +-- advances past a version the owner hasn't recorded), then broadcast the matching blob with the change projected +-- onto the served roster (so it excludes demoted/removed members). Returns the reserved version for the delta +-- that follows. The blob send is best-effort - a failed send heals on the next change or on resume. +broadcastRoster :: User -> GroupInfo -> RosterDelta -> CM VersionRoster +broadcastRoster user gInfo delta = do let rosterVer = maybe (VersionRoster 0) (\(VersionRoster n) -> VersionRoster (n + 1)) (rosterVersion gInfo) withStore' $ \db -> setGroupRosterVersion db gInfo rosterVer + sendRosterBlob rosterVer `catchAllErrors` eToView pure rosterVer - -broadcastRoster :: User -> GroupInfo -> VersionRoster -> CM () -broadcastRoster user gInfo rosterVer = do - cxt <- chatStoreCxt - (relays, rosterMems) <- withStore' $ \db -> - (,) <$> getGroupRelayMembers db cxt user gInfo <*> getGroupRosterMembers db cxt user gInfo - forM_ (L.nonEmpty relays) $ \relays' -> - sendRoster user gInfo (L.toList relays') rosterVer (buildGroupRoster rosterMems) + where + sendRosterBlob rosterVer = do + cxt <- chatStoreCxt + (relays, rosterMems) <- withStore' $ \db -> + (,) <$> getGroupRelayMembers db cxt user gInfo <*> getGroupRosterMembers db cxt user gInfo + forM_ (L.nonEmpty relays) $ \relays' -> + sendRoster user gInfo (L.toList relays') rosterVer (buildGroupRoster $ applyRosterDelta delta rosterMems) -- Send the current roster (no version bump) to a newly added relay so it can serve joiners. sendGroupRosterToRelay :: User -> GroupInfo -> GroupMember -> CM () diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 9cc6778122..408fae8105 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1086,11 +1086,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpMemIntro memInfo memRestrictions_ -> Nothing <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_ XGrpMemInv memId introInv -> Nothing <$ xGrpMemInv gInfo' m'' memId introInv XGrpMemFwd memInfo introInv -> Nothing <$ xGrpMemFwd gInfo' m'' memInfo introInv - XGrpMemRole memId memRole memberKey rosterVer -> fmap ctx <$> xGrpMemRole gInfo' m'' memId memRole memberKey rosterVer msg brokerTs + XGrpMemRole memId memRole memberKey rosterVer -> fmap ctx <$> xGrpMemRole gInfo' Nothing m'' memId memRole memberKey rosterVer msg brokerTs XGrpMemRestrict memId memRestrictions -> fmap ctx <$> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs XGrpMemCon memId -> Nothing <$ xGrpMemCon gInfo' m'' memId XGrpMemDel memId withMessages rosterVer -> case encoding @e of - SJson -> fmap ctx <$> xGrpMemDel gInfo' m'' memId withMessages rosterVer verifiedMsg msg brokerTs False + SJson -> fmap ctx <$> xGrpMemDel gInfo' Nothing m'' memId withMessages rosterVer verifiedMsg msg brokerTs False SBinary -> pure Nothing XGrpLeave -> fmap ctx <$> xGrpLeave gInfo' m'' msg brokerTs XGrpDel -> Just (DeliveryTaskContext (DJSGroup {jobSpec = DJRelayRemoved}) False) <$ xGrpDel gInfo' m'' msg brokerTs @@ -1098,6 +1098,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpPrefs ps' -> fmap ctx <$> xGrpPrefs gInfo' m'' ps' msg XGrpRoster gr -> fmap ctx <$> xGrpRoster gInfo' m'' m'' gr verifiedMsg sharedMsgId_ brokerTs XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck gInfo' m'' ackVer ackErr + XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest gInfo' m'' reqVer -- TODO [knocking] why don't we forward these messages? XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward gInfo' Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs @@ -3239,29 +3240,57 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- batch), then advance it in the same transaction; a strictly lower version is a replay and is ignored. -- Only an owner sender may advance it: a non-owner signed event is rejected by the action that follows, -- but must not bump roster_version first, or every later owner roster at a lower version is dropped. - applyAtRosterVersion :: GroupInfo -> GroupMember -> Maybe VersionRoster -> CM (Maybe DeliveryJobScope) -> CM (Maybe DeliveryJobScope) - applyAtRosterVersion gInfo sender rosterVer_ action + applyAtRosterVersion :: GroupInfo -> Maybe GroupMember -> GroupMember -> Maybe VersionRoster -> CM (Maybe DeliveryJobScope) -> CM (Maybe DeliveryJobScope) + applyAtRosterVersion gInfo fwdRelay_ sender rosterVer_ action | not (useRelays' gInfo) = action | otherwise = case rosterVer_ of Nothing -> action Just _ | memberRole' sender /= GROwner -> action Just v -> do - accept <- withStore' $ \db -> do - cur <- getGroupRosterVersion db gInfo - let fresh = maybe True (v >=) cur - when fresh $ setGroupRosterVersion db gInfo v - pure fresh + (accept, prevComplete) <- withStore' $ \db -> do + gate <- getGroupRosterVersion db gInfo + prevComplete <- getCompleteRosterVersion db gInfo + let fresh = maybe True (v >=) gate + when fresh $ do + setGroupRosterVersion db gInfo v + -- advance the frontier when this delta is the next version. One version can carry several deltas + -- (a multi-member role change), delivered in order, so seeing any one of them advances the frontier + -- past that whole version. + when (v == nextCompleteVersion prevComplete) $ + setCompleteRosterVersion db gInfo v + pure (fresh, prevComplete) if accept - then action + then (requestRosterOnGap v prevComplete `catchAllErrors` eToView) >> action else messageWarning "x.grp.mem: roster version not newer than current, ignoring" $> Nothing + where + -- the next contiguous version after the complete frontier. With no frontier the baseline is the first + -- roster version (VersionRoster 0, see broadcastRoster): a subscriber seeing v0 without a prior roster is + -- current, not gapped, as it cannot have missed an earlier version - so v0 neither requests nor stays behind. + nextCompleteVersion = \case + Just (VersionRoster c) -> VersionRoster (c + 1) + Nothing -> VersionRoster 0 + -- a subscriber whose complete frontier (before this delta) lags more than one below it has missed versions: + -- ask the relay that forwarded it (it holds >= v = the new gate) to re-serve the full roster, carrying the + -- previous frontier so only a fuller snapshot is served. A stuck frontier re-asks on every following delta + -- until a roster fills it. Best-effort; relays and the direct path (fwdRelay_ = Nothing) don't ask, nor a + -- relay that predates roster support. + requestRosterOnGap v prevComplete + | isUserGrpFwdRelay gInfo = pure () + | otherwise = case fwdRelay_ of + Just relay + | gap, relay `supportsVersion` groupRosterVersion -> + void $ sendGroupMessage' user gInfo [relay] (XGrpRosterRequest prevComplete) + _ -> pure () + where + gap = v > nextCompleteVersion prevComplete - xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole memberKey_ rosterVer_ msg@RcvMessage {msgSigned} brokerTs + xGrpMemRole :: GroupInfo -> Maybe GroupMember -> GroupMember -> MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) + xGrpMemRole gInfo@GroupInfo {membership} fwdRelay_ m@GroupMember {memberRole = senderRole} memId memRole memberKey_ rosterVer_ msg@RcvMessage {msgSigned} brokerTs | membershipMemId == memId = - applyAtRosterVersion gInfo m rosterVer_ $ + applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ let gInfo' = gInfo {membership = membership {memberRole = memRole}} in changeMemberRole gInfo' membership False (\db -> updateGroupMemberRole db user membership memRole) (RGEUserRole memRole) True - | otherwise = applyAtRosterVersion gInfo m rosterVer_ $ do + | otherwise = applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ do defaultRole <- unknownMemberRole gInfo -- an owner-signed event with a key TOFU-creates an unknown member only for a roster role; else a plain lookup let allowCreate = useRelays' gInfo && senderRole == GROwner && isRosterRole memRole && isJust memberKey_ @@ -3437,18 +3466,22 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = `catchAllErrors` \_ -> pure (cs, as) emitRosterResults :: GroupInfo -> GroupMember -> UTCTime -> ([MemberId], [(GroupMember, GroupMemberRole, Bool)]) -> CM () - emitRosterResults gInfo author rosterBrokerTs (conflicts, applied) = do + emitRosterResults gInfo@GroupInfo {membership} author rosterBrokerTs (conflicts, applied) = do forM_ conflicts $ \mid' -> messageWarning $ "x.grp.roster: member key conflict, keeping trusted key, memberId=" <> safeDecodeUtf8 (strEncode mid') forM_ applied $ \(member, fromRole, created) -> - unless created $ createItems member fromRole + unless created $ emitRoleChange member fromRole where - createItems member fromRole = do + emitRoleChange member fromRole = do let toRole = memberRole' member - gEvent = RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) toRole - (gInfo', author', scopeInfo) <- mkGroupChatScope gInfo author - ci <- createChatItem user (CDGroupRcv gInfo' scopeInfo author') False (CIRcvGroupEvent gEvent) Nothing (Just MSSVerified) (Just rosterBrokerTs) - toView $ CEvtNewChatItems user [ci] + (gInfo', author') <- + if sameMemberId (memberId' membership) member + then do + (gInfo', author', scopeInfo) <- mkGroupChatScope gInfo author + ci <- createChatItem user (CDGroupRcv gInfo' scopeInfo author') False (CIRcvGroupEvent $ RGEUserRole toRole) Nothing (Just MSSVerified) (Just rosterBrokerTs) + toView $ CEvtNewChatItems user [ci] + pure (gInfo', author') + else pure (gInfo, author) toView CEvtMemberRole {user, groupInfo = gInfo', byMember = author', member, fromRole, toRole, msgSigned = Just MSSVerified} sendRosterAck :: GroupInfo -> GroupMember -> VersionRoster -> Maybe Text -> CM () @@ -3474,6 +3507,19 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = messageError $ "x.grp.roster.ack: relay could not save roster, marked rejected: " <> e _ -> pure () + -- A relay re-serves the full roster to a subscriber that detected a version gap, but only when its STORED + -- blob is newer than BOTH the requester's version (Nothing = none) and the version it last served this member + -- - the latter bounds reflected amplification (a member can't re-trigger a full serve). Gating on the stored + -- blob (not roster_version, the gate) means the relay serves only a blob the requester will accept. + -- serveRoster records the served version (on all serve paths) and is a no-op without a roster. + xGrpRosterRequest :: GroupInfo -> GroupMember -> Maybe VersionRoster -> CM () + xGrpRosterRequest gInfo m reqVer_ = + when (isUserGrpFwdRelay gInfo) $ do + (stored_, served_) <- withStore' $ \db -> + (,) <$> getStoredRosterVersion db gInfo <*> getMemberRosterServedVersion db m + forM_ stored_ $ \stored -> + when (maybe True (stored >) reqVer_ && maybe True (stored >) served_) $ serveRoster user gInfo m + checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = when (memberRole < GRAdmin || memberRole < memRole) $ throwChatError (CEGroupContactRole localDisplayName) @@ -3518,11 +3564,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) - xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do + xGrpMemDel :: GroupInfo -> Maybe GroupMember -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) + xGrpMemDel gInfo@GroupInfo {membership} fwdRelay_ m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do let GroupMember {memberId = membershipMemId} = membership if membershipMemId == memId - then applyAtRosterVersion gInfo m rosterVer_ $ checkRole membership $ do + then applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ checkRole membership $ do deleteGroupLinkIfExists user gInfo -- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False @@ -3534,7 +3580,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = deleteMemberItem msg gInfo RGEUserDeleted toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned pure $ Just DJSGroup {jobSpec = DJRelayRemoved} - else applyAtRosterVersion gInfo m rosterVer_ $ + else applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Left _ -> do messageError "x.grp.mem.del with unknown member ID" @@ -3800,9 +3846,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XInfo p -> withAuthor XInfo_ $ \author -> void $ xInfoMember gInfo author p rcvMsg msgTs XGrpRelayNew rl -> withAuthor XGrpRelayNew_ $ \author -> void $ xGrpRelayNew gInfo author rl XGrpMemNew memInfo msgScope -> withAuthor XGrpMemNew_ $ \author -> void $ xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs - XGrpMemRole memId memRole memberKey rosterVer -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo author memId memRole memberKey rosterVer rcvMsg msgTs + XGrpMemRole memId memRole memberKey rosterVer -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo (Just m) author memId memRole memberKey rosterVer rcvMsg msgTs XGrpMemRestrict memId memRestrictions -> withAuthor XGrpMemRestrict_ $ \author -> void $ xGrpMemRestrict gInfo author memId memRestrictions rcvMsg msgTs - XGrpMemDel memId withMessages rosterVer -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo author memId withMessages rosterVer verifiedMsg rcvMsg msgTs True + XGrpMemDel memId withMessages rosterVer -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo (Just m) author memId withMessages rosterVer verifiedMsg rcvMsg msgTs True XGrpLeave -> withAuthor XGrpLeave_ $ \author -> void $ xGrpLeave gInfo author rcvMsg msgTs XGrpDel -> withAuthor XGrpDel_ $ \author -> void $ xGrpDel gInfo author rcvMsg msgTs XGrpInfo p' -> withAuthor XGrpInfo_ $ \author -> void $ xGrpInfo gInfo author p' rcvMsg msgTs diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 223fe492a9..2fb8361fdb 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -525,6 +525,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpRoster :: GroupRoster -> ChatMsgEvent 'Json XGrpRosterAck :: VersionRoster -> Maybe Text -> ChatMsgEvent 'Json + XGrpRosterRequest :: Maybe VersionRoster -> ChatMsgEvent 'Json XGrpMsgForward :: GrpMsgForward -> ChatMessage 'Json -> ChatMsgEvent 'Json XInfoProbe :: Probe -> ChatMsgEvent 'Json XInfoProbeCheck :: ProbeHash -> ChatMsgEvent 'Json @@ -1099,6 +1100,7 @@ data CMEventTag (e :: MsgEncoding) where XGrpDirectInv_ :: CMEventTag 'Json XGrpRoster_ :: CMEventTag 'Json XGrpRosterAck_ :: CMEventTag 'Json + XGrpRosterRequest_ :: CMEventTag 'Json XGrpMsgForward_ :: CMEventTag 'Json XInfoProbe_ :: CMEventTag 'Json XInfoProbeCheck_ :: CMEventTag 'Json @@ -1161,6 +1163,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpDirectInv_ -> "x.grp.direct.inv" XGrpRoster_ -> "x.grp.roster" XGrpRosterAck_ -> "x.grp.roster.ack" + XGrpRosterRequest_ -> "x.grp.roster.request" XGrpMsgForward_ -> "x.grp.msg.forward" XInfoProbe_ -> "x.info.probe" XInfoProbeCheck_ -> "x.info.probe.check" @@ -1224,6 +1227,7 @@ instance StrEncoding ACMEventTag where "x.grp.direct.inv" -> XGrpDirectInv_ "x.grp.roster" -> XGrpRoster_ "x.grp.roster.ack" -> XGrpRosterAck_ + "x.grp.roster.request" -> XGrpRosterRequest_ "x.grp.msg.forward" -> XGrpMsgForward_ "x.info.probe" -> XInfoProbe_ "x.info.probe.check" -> XInfoProbeCheck_ @@ -1283,6 +1287,7 @@ toCMEventTag msg = case msg of XGrpDirectInv {} -> XGrpDirectInv_ XGrpRoster _ -> XGrpRoster_ XGrpRosterAck {} -> XGrpRosterAck_ + XGrpRosterRequest _ -> XGrpRosterRequest_ XGrpMsgForward {} -> XGrpMsgForward_ XInfoProbe _ -> XInfoProbe_ XInfoProbeCheck _ -> XInfoProbeCheck_ @@ -1444,6 +1449,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" <*> opt "scope" XGrpRoster_ -> XGrpRoster <$> (GroupRoster <$> p "version" <*> p "fileInv") XGrpRosterAck_ -> XGrpRosterAck <$> p "version" <*> opt "error" + XGrpRosterRequest_ -> XGrpRosterRequest <$> opt "version" XGrpMsgForward_ -> do fwdSender <- opt "memberId" >>= \case Just memberId -> FwdMember memberId . fromMaybe "" <$> opt "memberName" @@ -1518,6 +1524,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XGrpDirectInv connReq content scope -> o $ ("content" .=? content) $ ("scope" .=? scope) ["connReq" .= connReq] XGrpRoster GroupRoster {version, fileInv} -> o ["version" .= version, "fileInv" .= fileInv] XGrpRosterAck version err -> o $ ("error" .=? err) ["version" .= version] + XGrpRosterRequest version -> o $ ("version" .=? version) [] XGrpMsgForward GrpMsgForward {fwdSender, fwdBrokerTs} msg -> o $ encodeFwdSender fwdSender ["msg" .= msg, "msgTs" .= fwdBrokerTs] where encodeFwdSender = \case diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 7ed599007c..70e64f68d7 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -92,7 +92,12 @@ module Simplex.Chat.Store.Groups getPublishableGroupRelays, setGroupRosterVersion, getGroupRosterVersion, - getGroupRoster, + getStoredRosterVersion, + setMemberRosterServedVersion, + getMemberRosterServedVersion, + setCompleteRosterVersion, + getCompleteRosterVersion, + getStoredGroupRoster, RcvRosterTransfer (..), createRosterTransfer, getRosterTransferVersion, @@ -1499,21 +1504,57 @@ getGroupRosterVersion db GroupInfo {groupId} = fmap join . maybeFirstRow fromOnly $ DB.query db "SELECT roster_version FROM groups WHERE group_id = ?" (Only groupId) --- The live roster header a relay re-serves to joiners, with the completed blob served alongside it --- (both are written together at completion, so the blob is present whenever the header is). -getGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg, Maybe ByteString)) -getGroupRoster db GroupInfo {groupId} = +-- The version of the roster blob actually stored (written with the blob in setGroupLiveRoster), as opposed to +-- roster_version (the acceptance gate). The owner sends the blob before the delta, so these normally match; a +-- failed blob send leaves the gate (advanced by the delta) ahead of the stored blob until a later roster completes. +getStoredRosterVersion :: DB.Connection -> GroupInfo -> IO (Maybe VersionRoster) +getStoredRosterVersion db GroupInfo {groupId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT stored_roster_version FROM groups WHERE group_id = ?" (Only groupId) + +-- The newest roster version a relay re-served to this member on its catch-up request: bounds reflected +-- amplification, so a member can't re-trigger a full serve at a version it was already served. +setMemberRosterServedVersion :: DB.Connection -> GroupMember -> VersionRoster -> IO () +setMemberRosterServedVersion db GroupMember {groupMemberId} v = do + currentTs <- getCurrentTime + DB.execute db "UPDATE group_members SET roster_served_version = ?, updated_at = ? WHERE group_member_id = ?" (v, currentTs, groupMemberId) + +getMemberRosterServedVersion :: DB.Connection -> GroupMember -> IO (Maybe VersionRoster) +getMemberRosterServedVersion db GroupMember {groupMemberId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT roster_served_version FROM group_members WHERE group_member_id = ?" (Only groupMemberId) + +-- The highest version up to which the subscriber holds a complete, contiguous picture: advances by 1 on a +-- contiguous delta and to the roster's version on a roster apply, but stays put on a gapped delta (so a stuck +-- value re-triggers the catch-up request on every following delta until a roster fills the gap). This is the +-- subscriber's "what I have" for both gap detection and the request - as opposed to roster_version (highest seen, +-- the revert gate) and stored_roster_version (the blob a relay holds). +setCompleteRosterVersion :: DB.Connection -> GroupInfo -> VersionRoster -> IO () +setCompleteRosterVersion db GroupInfo {groupId} v = do + currentTs <- getCurrentTime + DB.execute db "UPDATE groups SET applied_complete_roster_version = ?, updated_at = ? WHERE group_id = ?" (v, currentTs, groupId) + +getCompleteRosterVersion :: DB.Connection -> GroupInfo -> IO (Maybe VersionRoster) +getCompleteRosterVersion db GroupInfo {groupId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT applied_complete_roster_version FROM groups WHERE group_id = ?" (Only groupId) + +-- The live roster header a relay re-serves to joiners, with the completed blob and its stored version +-- (all written together at completion, so the blob and version are present whenever the header is). +-- Returns the stored version, not roster_version (the gate), so callers serve/record exactly what they hold. +getStoredGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg, Maybe ByteString, Maybe VersionRoster)) +getStoredGroupRoster db GroupInfo {groupId} = (>>= toRoster) <$> maybeFirstRow id ( DB.query db - "SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob FROM groups WHERE group_id = ?" + "SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob, stored_roster_version FROM groups WHERE group_id = ?" (Only groupId) ) where - toRoster (Just ownerGMId, Just brokerTs, Just cb, Just (Binary sigsBs), Just (Binary body), blob_) = - (\sigs -> (ownerGMId, brokerTs, SignedMsg cb sigs body, (\(Binary b) -> b) <$> blob_)) <$> eitherToMaybe (smpDecode sigsBs) + toRoster (Just ownerGMId, Just brokerTs, Just cb, Just (Binary sigsBs), Just (Binary body), blob_, storedVer_) = + (\sigs -> (ownerGMId, brokerTs, SignedMsg cb sigs body, (\(Binary b) -> b) <$> blob_, storedVer_)) <$> eitherToMaybe (smpDecode sigsBs) toRoster _ = Nothing -- A per-source in-flight roster transfer, keyed (group_id, from_member_id): replaces the single @@ -1598,6 +1639,10 @@ getRosterTransfer db fileId = -- Write the single live roster on groups from a completed transfer's values (header NULL on a member, -- so its live roster_msg_* stay NULL and it never re-serves; only relays re-serve). +-- Sets all three versions to the completed blob's version: the gate (roster_version - refuse anything older), +-- the stored version (stored_roster_version - the blob actually held and re-served), and the complete frontier +-- (applied_complete_roster_version - a snapshot makes the picture complete up to its version). Deltas advance the +-- gate always and the complete frontier only when contiguous, so complete <= stored <= roster_version normally. setGroupLiveRoster :: DB.Connection -> GroupInfo -> VersionRoster -> GroupMemberId -> UTCTime -> Maybe SignedMsg -> ByteString -> IO () setGroupLiveRoster db GroupInfo {groupId} v ownerGMId brokerTs sm_ blob = do currentTs <- getCurrentTime @@ -1605,13 +1650,13 @@ setGroupLiveRoster db GroupInfo {groupId} v ownerGMId brokerTs sm_ blob = do db [sql| UPDATE groups SET - roster_version = ?, roster_blob = ?, + roster_version = ?, stored_roster_version = ?, applied_complete_roster_version = ?, roster_blob = ?, roster_sending_owner_gm_id = ?, roster_broker_ts = ?, roster_msg_chat_binding = ?, roster_msg_signatures = ?, roster_msg_body = ?, updated_at = ? WHERE group_id = ? |] - ( (v, Binary blob, ownerGMId, brokerTs) + ( (v, v, v, Binary blob, ownerGMId, brokerTs) :. ((\SignedMsg {chatBinding} -> chatBinding) <$> sm_, (\SignedMsg {signatures} -> Binary (smpEncode signatures)) <$> sm_, (\SignedMsg {signedBody} -> Binary signedBody) <$> sm_, currentTs, groupId) ) diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index e17129cd1b..a3335640a2 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -39,6 +39,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at import Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain import Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster import Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name +import Simplex.Chat.Store.Postgres.Migrations.M20260629_roster_catchup import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -77,7 +78,8 @@ schemaMigrations = ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster), - ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name) + ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name), + ("20260629_roster_catchup", m20260629_roster_catchup, Just down_m20260629_roster_catchup) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_roster_catchup.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_roster_catchup.hs new file mode 100644 index 0000000000..f5b94e7d0b --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_roster_catchup.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260629_roster_catchup where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +-- Roster catch-up bookkeeping. Three monotonic per-group roster versions with distinct roles - normally equal, +-- diverging across gaps and failed transfers (applied_complete <= stored <= roster_version): +-- roster_version (added in M20260602) - the GATE: highest version seen. Advanced by every accepted owner delta +-- and by a roster apply. Revert protection: a completing roster older than this is rejected, since its +-- snapshot would undo a newer applied delta. +-- stored_roster_version - the blob HELD: the version of the roster blob actually stored (written with the blob). +-- What a relay can re-serve; a failed blob receive leaves it behind the gate (which the delta still advances) +-- until a later roster completes. Relay-side; on a member it is set but the blob is unused. +-- applied_complete_roster_version - the COMPLETE frontier: highest version up to which the picture is contiguous. +-- Advances by 1 on a contiguous delta and to the roster's version on apply, but stays put on a gapped delta. +-- The subscriber's "what I have" for gap detection and the catch-up request: a value below the gate means +-- missed versions, so each following delta re-asks the forwarding relay until a roster fills the frontier. +-- Also adds group_members.roster_served_version - the newest version a relay re-served a given member, bounding +-- reflected amplification (a member can't re-trigger a full serve at a version it was already served). +-- Backfill an existing roster's stored and complete versions from roster_version: pre-upgrade the picture is +-- contiguous up to roster_version (no gap detection existed), so a fresh NULL frontier would read every group's +-- next delta as a gap and make every subscriber request a re-serve at once. +m20260629_roster_catchup :: Text +m20260629_roster_catchup = + [r| +ALTER TABLE group_members ADD COLUMN roster_served_version BIGINT; +ALTER TABLE groups ADD COLUMN stored_roster_version BIGINT; +ALTER TABLE groups ADD COLUMN applied_complete_roster_version BIGINT; +UPDATE groups SET stored_roster_version = roster_version, applied_complete_roster_version = roster_version WHERE roster_version IS NOT NULL; +|] + +down_m20260629_roster_catchup :: Text +down_m20260629_roster_catchup = + [r| +ALTER TABLE group_members DROP COLUMN roster_served_version; +ALTER TABLE groups DROP COLUMN stored_roster_version; +ALTER TABLE groups DROP COLUMN applied_complete_roster_version; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 2799a79b81..3b8e7530c8 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -834,7 +834,8 @@ CREATE TABLE test_chat_schema.group_members ( member_relations_vector bytea, relay_link bytea, member_pub_key bytea, - removed_at timestamp with time zone + removed_at timestamp with time zone, + roster_served_version bigint ); @@ -994,7 +995,9 @@ CREATE TABLE test_chat_schema.groups ( roster_sending_owner_gm_id bigint, roster_broker_ts timestamp with time zone, roster_blob bytea, - group_domain_verified smallint + group_domain_verified smallint, + stored_roster_version bigint, + applied_complete_roster_version bigint ); diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 0cf78db595..d135c0f742 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -162,6 +162,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at import Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain import Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster import Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name +import Simplex.Chat.Store.SQLite.Migrations.M20260629_roster_catchup import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -323,7 +324,8 @@ schemaMigrations = ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster), - ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name) + ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name), + ("20260629_roster_catchup", m20260629_roster_catchup, Just down_m20260629_roster_catchup) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_roster_catchup.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_roster_catchup.hs new file mode 100644 index 0000000000..1dacc665ea --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_roster_catchup.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260629_roster_catchup where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +-- Roster catch-up bookkeeping. Three monotonic per-group roster versions with distinct roles - normally equal, +-- diverging across gaps and failed transfers (applied_complete <= stored <= roster_version): +-- roster_version (added in M20260602) - the GATE: highest version seen. Advanced by every accepted owner delta +-- and by a roster apply. Revert protection: a completing roster older than this is rejected, since its +-- snapshot would undo a newer applied delta. +-- stored_roster_version - the blob HELD: the version of the roster blob actually stored (written with the blob). +-- What a relay can re-serve; a failed blob receive leaves it behind the gate (which the delta still advances) +-- until a later roster completes. Relay-side; on a member it is set but the blob is unused. +-- applied_complete_roster_version - the COMPLETE frontier: highest version up to which the picture is contiguous. +-- Advances by 1 on a contiguous delta and to the roster's version on apply, but stays put on a gapped delta. +-- The subscriber's "what I have" for gap detection and the catch-up request: a value below the gate means +-- missed versions, so each following delta re-asks the forwarding relay until a roster fills the frontier. +-- Also adds group_members.roster_served_version - the newest version a relay re-served a given member, bounding +-- reflected amplification (a member can't re-trigger a full serve at a version it was already served). +-- Backfill an existing roster's stored and complete versions from roster_version: pre-upgrade the picture is +-- contiguous up to roster_version (no gap detection existed), so a fresh NULL frontier would read every group's +-- next delta as a gap and make every subscriber request a re-serve at once. +m20260629_roster_catchup :: Query +m20260629_roster_catchup = + [sql| +ALTER TABLE group_members ADD COLUMN roster_served_version INTEGER; +ALTER TABLE groups ADD COLUMN stored_roster_version INTEGER; +ALTER TABLE groups ADD COLUMN applied_complete_roster_version INTEGER; +UPDATE groups SET stored_roster_version = roster_version, applied_complete_roster_version = roster_version WHERE roster_version IS NOT NULL; +|] + +down_m20260629_roster_catchup :: Query +down_m20260629_roster_catchup = + [sql| +ALTER TABLE group_members DROP COLUMN roster_served_version; +ALTER TABLE groups DROP COLUMN stored_roster_version; +ALTER TABLE groups DROP COLUMN applied_complete_roster_version; +|] 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 af2fb0162d..5d7b56fc8f 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -5370,7 +5370,7 @@ SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE groups SET - roster_version = ?, roster_blob = ?, + roster_version = ?, stored_roster_version = ?, applied_complete_roster_version = ?, roster_blob = ?, roster_sending_owner_gm_id = ?, roster_broker_ts = ?, roster_msg_chat_binding = ?, roster_msg_signatures = ?, roster_msg_body = ?, updated_at = ? @@ -6665,6 +6665,36 @@ SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) +Query: DELETE FROM group_members WHERE member_id = ? +Plan: +SCAN group_members +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) + Query: DELETE FROM group_members WHERE user_id = ? AND group_id = ? Plan: SEARCH group_members USING COVERING INDEX idx_group_members_group_id (user_id=? AND group_id=?) @@ -7076,6 +7106,10 @@ Query: SELECT app_settings FROM app_settings Plan: SCAN app_settings +Query: SELECT applied_complete_roster_version FROM groups WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ? Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) @@ -7285,6 +7319,10 @@ Query: SELECT max(active_order) FROM users Plan: SEARCH users +Query: SELECT member_id FROM group_members WHERE local_display_name = ? +Plan: +SCAN group_members + Query: SELECT member_id FROM group_members WHERE member_role = ? LIMIT 1 Plan: SCAN group_members @@ -7313,6 +7351,10 @@ Query: SELECT member_role, member_pub_key FROM group_members WHERE local_display Plan: SCAN group_members +Query: SELECT member_role, member_pub_key FROM group_members WHERE member_id = ? +Plan: +SCAN group_members + Query: SELECT member_status FROM group_members WHERE local_display_name = ? Plan: SCAN group_members @@ -7361,10 +7403,14 @@ Query: SELECT roster_blob FROM groups WHERE roster_blob IS NOT NULL Plan: SCAN groups -Query: SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob FROM groups WHERE group_id = ? +Query: SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob, stored_roster_version FROM groups WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT roster_served_version FROM group_members WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT roster_transfer_id FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ? Plan: SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=? AND from_member_id=?) @@ -7389,6 +7435,10 @@ Query: SELECT should_sync FROM connections_sync WHERE connections_sync_id = 1 Plan: SEARCH connections_sync USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT stored_roster_version FROM groups WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT summary_current_members_count FROM groups WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) @@ -7633,6 +7683,10 @@ Query: UPDATE group_members SET member_role = ?, member_pub_key = NULL WHERE loc Plan: SCAN group_members +Query: UPDATE group_members SET roster_served_version = ?, updated_at = ? WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET support_chat_items_member_attention = ?, updated_at = ? WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -7653,6 +7707,14 @@ Query: UPDATE group_relays SET relay_status = ?, updated_at = ? WHERE group_rela Plan: SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET applied_complete_roster_version = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET applied_complete_roster_version = ?, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 954660dce5..01d53ad000 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -204,7 +204,9 @@ CREATE TABLE groups( roster_sending_owner_gm_id INTEGER, roster_broker_ts TEXT, roster_blob BLOB, - group_domain_verified INTEGER, -- received + group_domain_verified INTEGER, + stored_roster_version INTEGER, + applied_complete_roster_version INTEGER, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -249,6 +251,7 @@ CREATE TABLE group_members( relay_link BLOB, member_pub_key BLOB, removed_at TEXT, + roster_served_version INTEGER, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 51fe8c54aa..18023ed12c 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -300,6 +300,8 @@ chatGroupTests = do it "concurrent fresh invitations both rejected" testRelayRejectRaceConcurrentInvitations describe "promoted members roster" $ do it "moderator action verifies via owner-signed roster" testChannelModeratorActionViaRoster + it "subscriber recovers a missed roster member after a version gap" testChannelSubscriberRosterCatchUp + it "2 relays: subscriber recovers a missed roster member after a version gap" testChannel2RelaysSubscriberRosterCatchUp it "removed moderator drops from the roster cache" testChannelRemovedModeratorRefreshesRoster it "role transitions update the roster (mod <-> admin, admin -> non-roster)" testChannelRoleTransitionsUpdateRoster it "malicious relay cannot downgrade or re-key a roster-established moderator via XGrpMemNew" testChannelRelayCannotDowngradeRosterMember @@ -9710,6 +9712,24 @@ checkMemberRow cc name expectedRole = do DB.query db "SELECT member_role FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] map (\(Only r) -> r) roles `shouldBe` maybeToList expectedRole +-- The wire member id for a named member (look it up on a client that knows the name, e.g. the owner), used to +-- find a member by id on a subscriber that only knows it by member-id hash (e.g. after roster recovery). +getMemberIdByName :: TestCC -> T.Text -> IO ByteString +getMemberIdByName cc name = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_id FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only ByteString] + case rows of + [Only mid] -> pure mid + _ -> fail $ "expected one group_members row for " <> T.unpack name + +getMemberRoleKey :: TestCC -> ByteString -> IO (T.Text, Maybe ByteString) +getMemberRoleKey cc mid = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_role, member_pub_key FROM group_members WHERE member_id = ?" (Only mid) :: IO [(T.Text, Maybe ByteString)] + case rows of + [r] -> pure r + _ -> fail "expected one group_members row by member id" + testChannelModeratorActionViaRoster :: HasCallStack => TestParams -> IO () testChannelModeratorActionViaRoster ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9780,6 +9800,99 @@ testChannelModeratorActionViaRoster ps = DB.query db "SELECT member_role FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] map (\(Only r) -> r) roles `shouldBe` [expectedRole] +testChannelSubscriberRosterCatchUp :: HasCallStack => TestParams -> IO () +testChannelSubscriberRosterCatchUp ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + forM_ [cath, dan, eve, frank] $ \member -> + memberJoinChannel "team" [bob] [alice] shortLink fullLink member + -- promote dan (roster v0) then eve (v1) into the owner-signed roster; cath learns both with their keys + threadDelay 1000000 + promoteChannelMember "team" alice bob dan [cath, eve, frank] + threadDelay 1000000 + promoteChannelMember "team" alice bob eve [cath, dan, frank] + threadDelay 1000000 + -- simulate cath having fallen behind and lost dan: capture dan's member id (from the owner, which + -- knows the name) and cath's owner-pinned key for dan, then delete dan's record and rewind cath's + -- applied frontier so the next delta arrives as a gap (v2 > applied 0 + 1) + danId <- getMemberIdByName alice "dan" + (_, danKey) <- getMemberRoleKey cath danId + withCCTransaction cath $ \db -> do + DB.execute db "DELETE FROM group_members WHERE member_id = ?" (Only danId) + DB.execute db "UPDATE groups SET applied_complete_roster_version = ? WHERE group_id = ?" (0 :: Int64, 1 :: Int64) + -- the next privileged change (frank -> v2) reaches cath at a jumped version, triggering catch-up: + -- cath requests the roster from the forwarding relay, which re-serves the current snapshot + promoteChannelMember "team" alice bob frank [cath, dan, eve] + threadDelay 2000000 -- wait for the gap request + relay re-serve to recover dan + -- cath recovered dan from the re-served roster: same member id, role, and owner-pinned key + (recRole, recKey) <- getMemberRoleKey cath danId + recRole `shouldBe` "member" + recKey `shouldBe` danKey + +-- Same recovery, but the subscriber (frank) is connected to two relays: the request goes to whichever relay +-- forwarded the gapping delta, and only an observation that catch-up works in a 2-relay channel (not the race). +testChannel2RelaysSubscriberRosterCatchUp :: HasCallStack => TestParams -> IO () +testChannel2RelaysSubscriberRosterCatchUp ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + forM_ [dan, eve, frank] $ \member -> + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink member + -- promote dan (v0) then eve (v1) into the owner-signed roster, forwarded by both relays; frank + -- (the subscriber) learns both with their keys + threadDelay 1000000 + alice ##> "/mr #team dan member" + alice <## "#team: you changed the role of dan to member (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of dan from observer to member (signed)", + cath <## "#team: alice changed the role of dan from observer to member (signed)", + dan <## "#team: alice changed your role from observer to member (signed)", + eve <### [EndsWith "from observer to member (signed)"], + frank <### [EndsWith "from observer to member (signed)"] + ] + threadDelay 1000000 + alice ##> "/mr #team eve member" + alice <## "#team: you changed the role of eve to member (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of eve from observer to member (signed)", + cath <## "#team: alice changed the role of eve from observer to member (signed)", + eve <## "#team: alice changed your role from observer to member (signed)", + dan <### [EndsWith "from observer to member (signed)"], + frank <### [EndsWith "from observer to member (signed)"] + ] + threadDelay 1000000 + -- simulate frank having fallen behind and lost dan: delete dan's record and rewind frank's complete + -- frontier so the next delta (eve -> v2) arrives as a gap (2 > applied 0 + 1) + danId <- getMemberIdByName alice "dan" + (_, danKey) <- getMemberRoleKey frank danId + withCCTransaction frank $ \db -> do + DB.execute db "DELETE FROM group_members WHERE member_id = ?" (Only danId) + DB.execute db "UPDATE groups SET applied_complete_roster_version = ? WHERE group_id = ?" (0 :: Int64, 1 :: Int64) + -- eve -> moderator (v2) reaches frank at a jumped version; it requests the roster from the relay that + -- forwarded the delta, which re-serves the current snapshot (including dan), recovering dan + alice ##> "/mr #team eve moderator" + alice <## "#team: you changed the role of eve to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of eve from member to moderator (signed)", + cath <## "#team: alice changed the role of eve from member to moderator (signed)", + eve <## "#team: alice changed your role from member to moderator (signed)", + dan <### [EndsWith "from member to moderator (signed)"], + frank <### [EndsWith "from member to moderator (signed)"] + ] + threadDelay 2000000 -- wait for the gap request + relay re-serve to recover dan + (recRole, recKey) <- getMemberRoleKey frank danId + recRole `shouldBe` "member" + recKey `shouldBe` danKey + testChannelRemovedModeratorRefreshesRoster :: HasCallStack => TestParams -> IO () testChannelRemovedModeratorRefreshesRoster ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9804,6 +9917,8 @@ testChannelRemovedModeratorRefreshesRoster ps = threadDelay 1000000 alice ##> "/rm #team cath" alice <## "#team: you removed cath from the group (signed)" + -- the relay applies the removal via the roster (revert to observer) before the delete delta + bob <## "#team: alice changed the role of cath from moderator to observer (signed)" bob <## "#team: alice removed cath from the group (signed)" cath <## "#team: alice removed you from the group (signed)" cath <## "use /d #team to delete the group" @@ -10024,6 +10139,8 @@ testChannelRemoveMemberSigned ps = threadDelay 1000000 alice ##> "/rm #team eve" alice <## "#team: you removed eve from the group (signed)" + -- the relay applies the removal via the roster (revert to observer) before the delete delta + bob <## "#team: alice changed the role of eve from member to observer (signed)" bob <## "#team: alice removed eve from the group (signed)" concurrentlyN_ [ cath <## "#team: alice removed eve from the group (signed)", From 712effe34f55cefee3c8d0e9a71bc7327fadf67a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 4 Jul 2026 12:16:45 +0100 Subject: [PATCH 18/21] core: use concurrency-safe agent async api (#7035) * core: use concurrency-safe agent async api * simplexmq * simplexmq * combine transactions (build fails, names need to be merged first) * fix build * update simplexmq * remove uncurry * remove createAgentConnectionAsync * remove prepareJoinMemberContact * mapM * fix tests --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Library/Internal.hs | 70 +++++++++++++++++--------- src/Simplex/Chat/Library/Subscriber.hs | 39 ++++++++------ tests/Bots/DirectoryTests.hs | 4 +- 5 files changed, 73 insertions(+), 44 deletions(-) diff --git a/cabal.project b/cabal.project index bd8e4ac384..fb8ac872d1 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 836254a4c6bd5e17b40acbcaf77a83802d44919e + tag: a45d764eaa82fb4f26f8dfeb537c1464c8dad972 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 4ca9cefc7a..080a1ec635 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."836254a4c6bd5e17b40acbcaf77a83802d44919e" = "1g4385sv7i1h63yk3ch2kw8hprak1dzvr34v9mn6xz0fy10g8xlg"; + "https://github.com/simplex-chat/simplexmq.git"."a45d764eaa82fb4f26f8dfeb537c1464c8dad972" = "1f76904lwhxz9jkix0viaj715a88dlhr0x9v7jz79javcz57zcxp"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 62ce525cbd..20d0d10292 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -929,13 +929,15 @@ acceptContactRequestAsync profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True cxt <- chatStoreCxt let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - (cmdId, acId) <- agentAcceptContactAsync user True cReqInvId (XInfo profileToSend) subMode cReqPQSup chatV + (cmdId, acId) <- prepareAgentAccept user True cReqInvId cReqPQSup currentTs <- liftIO getCurrentTime - withStore $ \db -> do + ct' <- withStore $ \db -> do forM_ xContactId $ \xcId -> liftIO $ setContactAcceptedXContactId db ct xcId Connection {connId} <- liftIO $ createAcceptedContactConn db user (Just uclId) contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs liftIO $ setCommandConnId db user cmdId connId getContact db cxt user contactId + agentAcceptContactAsync cmdId acId True cReqInvId (XInfo profileToSend) cReqPQSup chatV subMode + pure ct' acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> Maybe GroupMember -> CM GroupMember acceptGroupJoinRequestAsync @@ -983,10 +985,12 @@ acceptGroupJoinRequestAsync } subMode <- chatReadVar subscriptionMode let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV - withStore $ \db -> do - liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + (cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff + m <- withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode getGroupMemberById db cxt user groupMemberId + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode + pure m acceptGroupJoinSendRejectAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupRejectionReason -> CM GroupMember acceptGroupJoinSendRejectAsync @@ -1013,10 +1017,12 @@ acceptGroupJoinSendRejectAsync } subMode <- chatReadVar subscriptionMode let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - connIds <- agentAcceptContactAsync user False cReqInvId msg subMode PQSupportOff chatV - withStore $ \db -> do - liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + (cmdId, acId) <- prepareAgentAccept user False cReqInvId PQSupportOff + m <- withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode getGroupMemberById db cxt user groupMemberId + agentAcceptContactAsync cmdId acId False cReqInvId msg PQSupportOff chatV subMode + pure m acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember) acceptBusinessJoinRequestAsync @@ -1045,10 +1051,11 @@ acceptBusinessJoinRequestAsync } subMode <- chatReadVar subscriptionMode let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV + (cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff withStore' $ \db -> do forM_ xContactId $ \xcId -> setBusinessChatAcceptedXContactId db gInfo xcId - createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode let cd = CDGroupSnd gInfo Nothing -- TODO [short links] move to profileContactRequest? createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing @@ -1071,12 +1078,14 @@ acceptRelayJoinRequestAsync subMode <- chatReadVar subscriptionMode cxt <- chatStoreCxt let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV - withStore $ \db -> do - liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + (cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff + r <- withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode gInfo' <- liftIO $ updateRelayOwnStatusFromTo db gInfo RSInvited RSAccepted ownerMember' <- getGroupMemberById db cxt user groupMemberId pure (gInfo', ownerMember') + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode + pure r rejectRelayInvitationAsync :: User @@ -1096,9 +1105,10 @@ rejectRelayInvitationAsync user uclId cxt groupRelayInv invId reqChatVRange init subMode <- chatReadVar subscriptionMode chatVR <- chatVersionRange let chatV = chatVR `peerConnChatVersion` reqChatVRange - connIds <- agentAcceptContactAsync user False invId msg subMode PQSupportOff chatV + (cmdId, acId) <- prepareAgentAccept user False invId PQSupportOff withStore' $ \db -> - createJoiningMemberConnection db user uclId connIds chatV reqChatVRange groupMemberId subMode + createJoiningMemberConnection db user uclId (cmdId, acId) chatV reqChatVRange groupMemberId subMode + agentAcceptContactAsync cmdId acId False invId msg PQSupportOff chatV subMode businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = @@ -2752,18 +2762,24 @@ msgContentHasLink mc ft_ = case msgContentTag mc of MCLink_ -> True _ -> maybe False hasLinks ft_ -createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId) -createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do +prepareAgentCreation :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> CM (CommandId, ConnId) +prepareAgentCreation user cmdFunction enableNtfs cMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction - connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode IKPQOff subMode + connId <- withAgent $ \a -> prepareConnectionToCreate a (aUserId user) enableNtfs cMode PQSupportOff pure (cmdId, connId) -joinAgentConnectionAsync :: User -> Maybe Connection -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM (CommandId, ConnId) -joinAgentConnectionAsync user conn_ enableNtfs cReqUri cInfo subMode = do +prepareAgentJoin :: User -> Maybe Connection -> Bool -> ConnectionRequestUri c -> CM (CommandId, ConnId) +prepareAgentJoin user conn_ enableNtfs cReqUri = do cmdId <- withStore' $ \db -> createCommand db user (dbConnId <$> conn_) CFJoinConn - connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) (aConnId <$> conn_) enableNtfs cReqUri cInfo PQSupportOff subMode + connId <- case conn_ of + Just conn -> pure $ aConnId conn + Nothing -> withAgent $ \a -> prepareConnectionToJoin a (aUserId user) enableNtfs cReqUri PQSupportOff pure (cmdId, connId) +joinAgentConnectionAsync :: ConnectionModeI c => CommandId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM () +joinAgentConnectionAsync cmdId updateConn connId enableNtfs cReqUri cInfo subMode = + withAgent $ \a -> joinConnectionAsync a (aCorrId cmdId) updateConn connId enableNtfs cReqUri cInfo PQSupportOff subMode + allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM () allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn @@ -2771,13 +2787,17 @@ allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersi withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm withStore' $ \db -> updateConnectionStatus db conn ConnAccepted -agentAcceptContactAsync :: MsgEncodingI e => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> PQSupport -> VersionChat -> CM (CommandId, ConnId) -agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do +prepareAgentAccept :: User -> Bool -> InvitationId -> PQSupport -> CM (CommandId, ConnId) +prepareAgentAccept user enableNtfs invId pqSup = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact - dm <- encodeConnInfoPQ pqSup chatV msg - connId <- withAgent $ \a -> acceptContactAsync a (aUserId user) (aCorrId cmdId) enableNtfs invId dm pqSup subMode + connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) enableNtfs invId pqSup pure (cmdId, connId) +agentAcceptContactAsync :: MsgEncodingI e => CommandId -> ConnId -> Bool -> InvitationId -> ChatMsgEvent e -> PQSupport -> VersionChat -> SubscriptionMode -> CM () +agentAcceptContactAsync cmdId connId enableNtfs invId msg pqSup chatV subMode = do + dm <- encodeConnInfoPQ pqSup chatV msg + withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) connId enableNtfs invId dm pqSup subMode + deleteAgentConnectionAsync :: ConnId -> CM () deleteAgentConnectionAsync acId = deleteAgentConnectionAsync' acId False {-# INLINE deleteAgentConnectionAsync #-} diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 408fae8105..52d70f1fae 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -639,9 +639,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do groupInfo <- withStore $ \db -> getGroupInfo db cxt user groupId subMode <- chatReadVar subscriptionMode - groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode + groupConnIds@(cmdId, connId) <- prepareAgentCreation user CFCreateConnGrpInv True SCMInvitation gVar <- asks random withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds connChatVersion peerChatVRange subMode + withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) connId True SCMInvitation CR.IKPQOff subMode -- TODO REMOVE LEGACY ^^^ SENT msgId proxy -> do void $ continueSending connEntity conn @@ -1242,7 +1243,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo incognitoProfile dm <- encodeXMemberConnInfo gInfo relayMemberId profileToSend subMode <- chatReadVar subscriptionMode - void $ joinAgentConnectionAsync user (Just conn) True cReq dm subMode + (cmdId, connId') <- prepareAgentJoin user (Just conn) True cReq + joinAgentConnectionAsync cmdId True connId' True cReq dm subMode CFGetRelayDataAccept -> do let GroupMember {memberId = MemberId expectedMemberId} = m if linkEntityId == Just expectedMemberId @@ -1639,12 +1641,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = let sig = C.signatureBytes $ C.sign' privKey challenge msg = XGrpRelayTest challenge (Just sig) subMode <- chatReadVar subscriptionMode - chatVR <- chatVersionRange - let chatV = chatVR `peerConnChatVersion` chatVRange - (cmdId, acId) <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV + let chatV = vr cxt `peerConnChatVersion` chatVRange + (cmdId, acId) <- prepareAgentAccept user True invId PQSupportOff withStore $ \db -> do Connection {connId = testCId} <- createRelayTestConnection db cxt user acId ConnAccepted chatV subMode liftIO $ setCommandConnId db user cmdId testCId + agentAcceptContactAsync cmdId acId True invId msg PQSupportOff chatV subMode | otherwise = messageError "relay test sent to non-relay link" where User {userChatRelay} = user @@ -2610,12 +2612,13 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = then do subMode <- chatReadVar subscriptionMode dm <- encodeConnInfo $ XGrpAcpt membershipMemId - connIds <- joinAgentConnectionAsync user Nothing True connRequest dm subMode + connIds@(cmdId, acId) <- prepareAgentJoin user Nothing True connRequest withStore' $ \db -> do setViaGroupLinkUri db groupId connId createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode updateGroupMemberStatusById db userId hostId GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted + joinAgentConnectionAsync cmdId False acId True connRequest dm subMode toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) else do let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole @@ -3176,16 +3179,14 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Just (ChatVersionRange mcvr) | maxVersion mcvr >= groupDirectInvVersion -> do subMode <- chatReadVar subscriptionMode - -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second - groupConnIds <- createConn subMode + groupConnIds@(cmdId, connId) <- prepareAgentCreation user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation let chatV = maybe (minVersion (vr cxt)) (\peerVR -> vr cxt `peerConnChatVersion` fromChatVRange peerVR) memChatVRange void $ withStore $ \db -> do reMember <- createIntroReMember db cxt user gInfo memInfo memRestrictions createIntroReMemberConn db user m reMember chatV memInfo groupConnIds subMode + withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) connId (chatHasNtfs chatSettings) SCMInvitation CR.IKPQOff subMode | otherwise -> messageError "x.grp.mem.intro: member chat version range incompatible" _ -> messageError "x.grp.mem.intro can be only sent by host member" - where - createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation subMode sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> CM () sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do @@ -3228,12 +3229,16 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile -- [async agent commands] no continuation needed, but commands should be asynchronous for stability - groupConnIds <- joinAgentConnectionAsync user Nothing (chatHasNtfs chatSettings) groupConnReq dm subMode - directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user Nothing True dcr dm subMode + let enableNtfsGrp = chatHasNtfs chatSettings + groupConnIds@(gCmdId, gAcId) <- prepareAgentJoin user Nothing enableNtfsGrp groupConnReq + directConnIds <- mapM (prepareAgentJoin user Nothing True) directConnReq let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo mcvr = maybe chatInitialVRange fromChatVRange memChatVRange chatV = vr cxt `peerConnChatVersion` mcvr withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode + joinAgentConnectionAsync gCmdId False gAcId enableNtfsGrp groupConnReq dm subMode + forM_ ((,) <$> directConnIds <*> directConnReq) $ \((dCmdId, dAcId), dcr) -> + joinAgentConnectionAsync dCmdId False dAcId True dcr dm subMode -- rollback defense (channels): apply an owner-signed role/removal only at a version >= the persisted -- roster_version (not the batch-constant gInfo, which a relay can stale by reordering events in one @@ -3753,11 +3758,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = } joinExistingContact subMode mCt@Contact {contactId = mContactId} | autoAcceptMemberContacts user = do - (cmdId, acId) <- joinConn subMode + (cmdId, acId) <- prepareAgentJoin user Nothing True connReq mCt' <- withStore $ \db -> do updateMemberContactInvited db user mCt groupDirectInv void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode getContact db cxt user mContactId + joinMemberContactAsync cmdId acId subMode securityCodeChanged mCt' createItems mCt' m | otherwise = do @@ -3771,13 +3777,14 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = createItems mCt' m createNewContact subMode | autoAcceptMemberContacts user = do - (cmdId, acId) <- joinConn subMode + (cmdId, acId) <- prepareAgentJoin user Nothing True connReq -- [incognito] reuse membership incognito profile (mCt, m') <- withStore $ \db -> do (mContactId, m') <- liftIO $ createMemberContactInvited db user g m groupDirectInv void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode mCt <- getContact db cxt user mContactId pure (mCt, m') + joinMemberContactAsync cmdId acId subMode createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) createItems mCt m' | otherwise = do @@ -3790,12 +3797,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) createInternalChatItem user (CDDirectRcv mCt) (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing createItems mCt m' - joinConn subMode = do + joinMemberContactAsync cmdId acId subMode = do -- [incognito] send membership incognito profile p <- presentUserBadge user (incognitoMembershipProfile g) $ userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing True -- TODO PQ should negotitate contact connection with PQSupportOn? (use encodeConnInfoPQ) dm <- encodeConnInfo $ XInfo p - joinAgentConnectionAsync user Nothing True connReq dm subMode + joinAgentConnectionAsync cmdId False acId True connReq dm subMode createItems mCt' m' = do (g', m'', scopeInfo) <- mkGroupChatScope g m' createInternalChatItem user (CDGroupRcv g' scopeInfo m'') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 1c6da2e1ed..025bfe4f66 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -449,9 +449,9 @@ testJoinGroup ps = cath <## "" cath <## "Send captcha text to join the group privacy." captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory_1'> " . dropTime <$> getTermLine cath - cath #> ("#privacy (support) " <> captcha) cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" cath <## "use @'SimpleX Directory' to send messages" + cath #> ("#privacy (support) " <> captcha) cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" @@ -1277,6 +1277,8 @@ testKnocking ps = cath <## "#privacy: you joined the group, connecting to group moderators for admission to group" cath <## "#privacy: 'SimpleX Directory' accepted you to the group, pending review" bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting and pending review...), use /_accept member #1 3 to accept member" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: new member cath is connected and pending review, use /_accept member #1 3 to accept member" testCaptchaByDefault :: HasCallStack => TestParams -> IO () testCaptchaByDefault ps = From ff58dbd6dfb6f5d53783643522e7292cbd7efd63 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:25:20 +0400 Subject: [PATCH 19/21] docs: add SimpleX name registration guide (#7194) * docs: add SimpleX name registration guide * docs: clarify SimpleX name registration steps Explain why registration needs two on-chain transactions with a wait (commit-reveal prevents someone from grabbing the name mid-registration). Mark attaching both contact and channel links as advanced usage. * docs: switch name guide to official dapp, apply review Use testing-names.simplex.chat and its simpler flow (both SimpleX contact/channel fields on Create your profile; no Add profile fields modal). Apply @epoberezkin review suggestions (intro, testing page URL, 'main names launch', claim purpose). * Apply suggestions from code review Co-authored-by: Evgeny * docs: light screenshots, update name flow diagram Use light-theme official-dapp screenshots. Rework the flow diagram: drop the 'both' branch, split contact vs channel before step 4 (each set in its own window), and match the reviewed connect wording (search or type the name). --------- Co-authored-by: Evgeny --- docs/guide/README.md | 1 + docs/guide/diagrams/simplex-name-steps.mmd | 19 ++++ docs/guide/diagrams/simplex-name-steps.svg | 1 + docs/guide/images/before-we-start.png | Bin 0 -> 269753 bytes docs/guide/images/create-your-profile.png | Bin 0 -> 235606 bytes docs/guide/images/register-name.png | Bin 0 -> 232481 bytes docs/guide/register-simplex-name.md | 123 +++++++++++++++++++++ 7 files changed, 144 insertions(+) create mode 100644 docs/guide/diagrams/simplex-name-steps.mmd create mode 100644 docs/guide/diagrams/simplex-name-steps.svg create mode 100644 docs/guide/images/before-we-start.png create mode 100644 docs/guide/images/create-your-profile.png create mode 100644 docs/guide/images/register-name.png create mode 100644 docs/guide/register-simplex-name.md diff --git a/docs/guide/README.md b/docs/guide/README.md index ad3849d1d3..fe1546af0a 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -12,6 +12,7 @@ The first messaging platform that has no user identifiers of any kind — 100% p - [Secret groups](./secret-groups.md) - [Channel webpage](./channel-webpage.md) - [Chat profiles](./chat-profiles.md) +- [Registering a SimpleX name](./register-simplex-name.md) - [Managing data](./managing-data.md) - [Audio & video calls](./audio-video-calls.md) - [Privacy & security](./privacy-security.md) diff --git a/docs/guide/diagrams/simplex-name-steps.mmd b/docs/guide/diagrams/simplex-name-steps.mmd new file mode 100644 index 0000000000..ca19d3c6f3 --- /dev/null +++ b/docs/guide/diagrams/simplex-name-steps.mmd @@ -0,0 +1,19 @@ +flowchart TD + S([Start]) --> A["Step 1: Create a wallet
save your recovery phrase"] + A --> B["Step 2: Register the name
connect wallet, search, choose years"] + B --> C["On Create your profile, paste your link into
SimpleX contact (@name) or SimpleX channel (#name)"] + C --> H["Confirm: two transactions,
60 seconds apart"] + H --> I["Step 3: search or type your name
shows 'Unconfirmed name'"] + I --> D{"A contact or a channel?"} + D -->|"@name (contact)"| J1["Step 4: set Your SimpleX name
in your SimpleX address"] + D -->|"#name (channel)"| J2["Step 4: set SimpleX name
in the channel"] + J1 --> K["Step 5: from another device,
search or type the name"] + J2 --> K + K --> Z([Connected. Name verified.]) + + classDef start fill:#f4ecf7,stroke:#8e44ad,color:#4a235a; + classDef step fill:#eaf2fb,stroke:#2e86de,color:#1b3a5b; + classDef ok fill:#d5f5e3,stroke:#27ae60,color:#145a32; + class S start; + class Z ok; + class A,B,C,H,I,J1,J2,K step; diff --git a/docs/guide/diagrams/simplex-name-steps.svg b/docs/guide/diagrams/simplex-name-steps.svg new file mode 100644 index 0000000000..4d062ec1c2 --- /dev/null +++ b/docs/guide/diagrams/simplex-name-steps.svg @@ -0,0 +1 @@ +

@name (contact)

#name (channel)

Start

Step 1: Create a wallet
save your recovery phrase

Step 2: Register the name
connect wallet, search, choose years

On Create your profile, paste your link into
SimpleX contact (@name) or SimpleX channel (#name)

Confirm: two transactions,
60 seconds apart

Step 3: search or type your name
shows 'Unconfirmed name'

A contact or a channel?

Step 4: set Your SimpleX name
in your SimpleX address

Step 4: set SimpleX name
in the channel

Step 5: from another device,
search or type the name

Connected. Name verified.

\ No newline at end of file diff --git a/docs/guide/images/before-we-start.png b/docs/guide/images/before-we-start.png new file mode 100644 index 0000000000000000000000000000000000000000..eb853fcd968b57490e9d1ab0dd5525dde3b70249 GIT binary patch literal 269753 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 Rf=tjE&YGX6+4w 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 z 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%0Ab2$9C^V7E9lVArI|+ z=_s=*Q?AJm`^4rEe2^aXQ}C=g*H}lHzzt+F z$ec|P*jYe!S}eyj6X;TPCdjvP$CKq()Psf%da_*=l+nhtFD?grFhyg9B|90DTnrHQ zZv$NU`C#n�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 -WhhjJ%t#zEwDjXKLCN-GAK9EO zR)`w#dy0wH6OLM_WC9G*PS?%Zw^nKG9S*snJN{Ktc8;Se^uP0f#aP4p(`tp|Zg*;oe0a90Cu;`x+C$z}<@i3u*eA8b@if(I z8rDO%i4SYld+#MFfw5u|Wni(JzraU@Rc-h^2Xs|cyw0PS#iS$rZ0|mWFbkRQdPs2= zXMIMTqg5dMC{e&ci2s>9;2U zi}pJu|9FjiAueU0Y0eOr<>Y=PAf6(f6{^1bW3Jv|XVThZzqzJoL(O@22KmE>$N|iP zwlzK*SAeE^+F?kL((d~$z}iY<>E2~e>0o2C*9oZhU0N^Z0r_c-&1{X;)@be*%3mI> zwXO%tDGJr9b8lB7Y8TvfMp2(>el|Q#nYf8+K8lRWKA$;DSJRe~y4#GV*1&noJWCqy zqip$;mp|UMaJOGq`rdPHUl3`CU?RVBC!X35r!e|nZY=jcWvZ3wMnb)+GtacZB6M)0 z2e^qI^o@9~gBc#i**O4JbcQS@fSWJ;<;$<2RcnqH(05CA=2Zhy*y)LN1vV7Pk=yVo5%d4@73esY%C5-q0?LEu|V_wn>~>zgg$LT0i@XT;cj z|8i^nMeUqt<<*e^)a|e*j(+R1HRS#ldsh@JiN~kqnHyl$kxLfn`B>>-4;m2vyo$nz zq!3Jri+gbw9Shxj`199(Vvh5!t|Qdw#KhA!(A(hTq8IdOc!*ChiZ))L-|VulPZJzf z=idG^^U;&$!!2TBn9o7S{u7Pe#a5U%CoOHRElem!Qjg!U8>iOiYCr1*mS@k0LkJ;KN zH{oV?r;FoVm-)a0Jw6u#ST8;hCwa3F;O}qXeSm^UnHzoG2e;KMlMN0GY&rXOM0L6S z{rc(x#7qZW!B`ik`x;t{@2tiPlv`W8N=ZD`JzHDY+oqb$hBOR(N@P(PMar)B)sypE zfAtm-6K^%FR>O72@?C7tzi(vtwA4)*_bRFe!UdMkZ(xf6-+rE1xuJhK%gM%xZ zrt9-yEK{5bFks+Zw>!j8XcKpQ9~A)%cN?q_`OeFGC|F(YYHuvTil?mYJCK3$wL z>l#cq?ou3n%4Rl34f(p- z2U>QWC!1|uUb<0Yld%dP(jMGKrI42yeH?;KdUD<9klaQ2{H@{1ENG~4A=7gNx@Mom zgiw$Sx3->vE?gRNjW^zU?6kD2-B~`b^*?GXdOK!0c5)2Ox%UnH`f8V`t1UIbj(4Bp zE(QjFN<66R=8q)f6@^+})|J~fouu>|Cb2@JXo9hQZ1>1SyFmwq4A;X?fNR;xH3QTH zr`k3hTwml(6lirU}kA)8{1H?-qUlwLCbmpHl}bWvvIDhYz6~;hB7*FJ9!!Wq96r zjTvRSKzWaUR zV(4QMj;BlDj2YV_;lEhl@G4Q!ew;^>LmGdVYsB&ZMo~!uEZA$qvGsv|PuN1UmloD? zOj?@gT?FWcl)vCcSV_`O!@+Tsr(@&wkFOOwR8zI`K%fdsP~5%f!y^AhpsAyU{(@lPeVGa0q%~ zQ@?xi$z`EqRf#ieSV%OFMC>jjP|CA7#Zq#? z_4Y740{xLgPP_;AvB`y&>o;JD#i+QVhlhtkDLiG1zJ4HC!44g@?_z1acApr2)t68f z{Rw>G-o1N57neu48c`PdnL48h`maqqh3 zyK7A5CSpKDMDD(^n-7xSYSzqW7R%mJ9EU5sVJ1ia_?jkW@>_w@OpN9sy+q?oD&oUp z``AM3h6jH&{A*oF^g{9+rHh}Rt~e$Tv7!r z^R;f}94{?Pw!Sbt`hB18A{c@u&S(3r9DuL7Ze`Cy0VZU<9Tp}T6-mOs_q|nRS5>=M z{s_M4-RRuE*`6ag&_YH<%faEWo$k`i>rqqfqQJY969sL#STH~QXuxxm&dcHa&B>=2 zUe*in@6l(rU!ihdG+JbC-ib5{Bw1BkVOr_5Lom*rx9ox{4cex>4-!xzC{#-Rjeljq zFMBIr(dN1P(pvm5lE*v})3(gE5Q(essFkFpmiN&R#kDkwCRC861n#mL>gecb65?4p zAHtPZ?(pb{(tQDBT&TmOcl?5fTzE z-h6*?af0~8e+Fi%(7dA1qd-Sz+n;g_EYl2Sj}u*&~ z;IQB|)ExT(%0rM>zZZ3UD%Z%uKRbtEuB?8X&g-z~Z5Fx4ah3?E+FJnJC17at^75W8 zxZE79y!7rv;sir51@10m&$)6uov8Kq4{qmHfyq3hsCZ+Fqt9m!n9XnEwqg7{oWY+5l|I6+eNQJY+>duITxkWauQw=FyP*6l^;c;3St;Lz zh1IXdzGP0}iHeL=kdcv*dhWT}jcZ!HRXGH0&DMeAwuNvE9W7k!gRED_ctKU}$B))? zRXvAe1F?yTE~kR0rmxL7D6L0xrFFvz?3lF!1A}R4$rPS@o`FLx_XIJr6;%ru)y?DR zoKeSeJWfsJQ%4DImLJPX>Uor$)wh=EO2ex}Ux;=_krv4caTs?dl<4ffAM2Q|Gz0bx zj=kvBr@J-SnslZ6T=dnerTqsV`6<9Q54!I0s`8U2r?1D*NaoG^w0;T}rLb*2QYd=K z?8f6~-v|DDz5gjc2^$3st=3|?0$HV(%GK99r~q0WZYqgdSTK2+n3}HZoh~%4<_BeE zW$9C}006eXHAKk%Ge*FRKF_Se2nJfRd9R#SBc7nDA^N6N~|*nnR>f=8F~!SaFa{LNrmGU5`We<3kZ-_5Bz znu=;%LdJY+%VEJQipMuIGt=AaLZ8d^Etsyckr6uwhhl~RqKk4zkeGL5m(!T!Wi)Rj>O6%(C%re|7d_0?;wOYar6a_DyHb+Ul zd^wp~Y1(=;X~hv|$olHr8C>7zMdI9NJ7aeV3C9v`e|mO!c;j|qkp;Z8WZ?2-c7M_H zf_$I0gr(7HwY$i}abLg8W**>feEj&l$I0dStnChul_5nd00JJf0q^ae8$8V7(FY_X ze2(4fx6e;)7mBq^)W+-IJ4nXGzAV!^y6FATBu?m~`}t>$j*aM6qQ8!sCV#kCwm+sQ1bPk1>Ul3IhU>ep zJ#0{STx{{eAb6I>8B@+r2zKvF+YUj>`?AL;UHy`at^xhsM@)SI0T!O~!XNLTP<`3D zRY7uGZE3B>Xw5i!>>MB(@+w0qrgvXC9=5dp84>PsV0L9iU5;QGoIq7`@nxah5fqt*7MrS{}C}G#VMP=7@hFR@T zWs^X|DVt^h>J5+701c`6>B18sX>bJ03_L|PLhjdZ&(}L_*#igAVMC!W@oC1vTtC2D z?bBhY>7^uL{{Fnt&YujA&pB1-lnXnS+~3avrS?TIxVg=8)D}PN?~`4J?YBM z&W0~|#<>NPa9K2+UG^)(^^5cjO2L>xn3K%zHae)+NRfW+YGg!x)jrgEd6=MMW20o_ zzWxUIP+cGlt(1&Pu(JbaVrgk9F#6FX3tVLR;@Il|u|di+*+iTYH<->JLC(RqHC0Xz zb9YC2NA%uj2Tir6h#Ju_>|G`%XtQ@~d%kf8^dr zu9&IGH0bjOZrM?VbY%~;^a#SFo;(1)cU4nDN&*AY({YvC9au527%w;&)T#8j0TIkz zLy-==6l)t^reC71`TL`!)B`ne0TI3Rh4Vb#ix)2zQ~4$5-0gR{;KO~9xT2t=rK(=u z=Yq{}mR6&TT(c+&sP|2JDak4w9p8BbT;Jo@QV2@hPgJCa%_on8j=Mbi9uZnt_PAtZ zm;Gak9UUE8hu~bnDtl$~3P86F&sJ|o?}O#X{$^KIZ|v;s%AZYxR)9}DPR7Zd&sdFZ z^Eb7t39irDHkEVm2X7o39rv1Cm;C$qopUK0Km~jKyn=*sy!q)|polv9zhB9SrCY_hzghOrxYi#@c8h)NDHD^s19zj> zZZM;&(Dl#T`qH39poO9wOz2{OCuu(dWM(I4GNip;AeUJv}$^Dg>&`AGU{&-kBErjMkF3q1&y&0#&Rp|v)f=iI*wmEE{Gw^zcTeXaV zV*o@46#ULyY$6K}m$pw#j9uhj?>#WU-3bj$ z7%3s4%c-8rC<2Srt9F(coFORn+mK?-HyIyL^L$<}QjUpJ=P)V`b<$32Pnf^Jjtliv z?$a*{TX>WW&$RdVS9;u$BV;9!C}xcv?Iz!EnY~%P9u-hq)jMXIEowL*1E2}x5<-H4B5s30qgp#4_G-< z4K5ssc`iF%w?zu1u&@rDX)Q^gzWwm#7nNjSVWIGvCo)aMabDG0KCrY59Q_LEOcz?u z!=vENHlA9^sIQ4fG)Z{iFjF&O zYQ0FA7mzP%M~TPLWK_kXlUXdz-*>V+1zmilD-kMtuL1vdfRx<)Imr*m87L_HI3nipB??ip&H;c zYWk7ef6wO|2tt-kO@lLeA#+Ocnp#I?vkk9tS?Qi8K-*o5I@k=l*3U_~&E=JpLisCA ze#x#f2Wr^2gsJQ&2-}#3{T|!!fEL~L_4O5gJMY=-J*yILJXfH>*zJRXLq1e)A|*I@ zLCzJK&Tv37+Mvyms8o3bA`n>ikjCd3ZUnWw} ztHo94__LCyk1l!pWkUb@_qJk{;X^OUM)`+u_j9a|J^ta%Uom^gERgqWkL4UEx+!4% zAnu#%wSD0xHrB6_j7+PcFti5pvJc-G1nY?_7XfhK%!87c=P^Bb+q#m5Nev>|;qDL; z4wa;zZ(S2p1U-{fa@~wlmY2=X61KGsz}ypcYg79kZjHEf4_oYa>N9XfzEi%0H@Pn@ zKZ&w>>OdE%0^h->@l{IUTx<;&9jCt%aa^C53IOmA!E0_YvGl2DycYX(hO1qXw1*}D z=r!NI*Omo3`h%)4<0i{6cJBly2Qdd|=jX6TeETQ_H`06Q{(}dj=RGVfS9@}XqR;4M z_U}cFXln6sMeZ4eg$r=$Ny=Yc?j6cX1H%u@rPJonkI@2;L0Hyv$NeJv=E#=mTeok+ zgq(76@!6@PjsbVwe)k2a!5GRvpmvkw(M^<2Lz8T()> zFIAiB_#20HW(_-AS>n-a+5VJx7gQp_YLE!pgxb7_g2}|{zzU_Cv$IAn9wbJpq4AMd zBiyC*vpQ0kQ(xZ)cua(7Z;Y1|^_gsJY&h-%G+SSI!h=Uq*1ywcM3)S+ds96-$Mf9T z*`H4J5h96P%s*_D{O#a;<8`cwoqW?}LefdYqZT`wGX322+Ldgo<@X zaz)$^hv+q4kj55*fJ?sg;c6~0(vA{L^jbCki)5wig55Sv;Juauo9YD zMD|##k`760gXvcM0oC?a+m+)r%E=203z?ZThN3d{pH}3Che>y?yf3G$4ywS5D<|`Q zg`(?rR>;#h=yoHeo1^no+Yc&O-7TudPaUL_*G^qyCK$Q6p1oezCukb%-jxZGDT^IH z%kL$WOb{*KbjP@bsl#15tnQHalF#M!$Co|a+w#0nsPZj6leM6w0<1h-)s#`@9L4Pt z>1csqdKdFdSfj8Awb9(%0~}hgWP8L1DQ6RbO%{L^g`N6=lUuvVJ6OFY<+LxlUJ>~^ zV#kxdm|otCGa>FKf69iS(OIZRFbs9m_r`6^xnk=e+LhCIgV-6-aH^c%H#k={sj5lZ zO!#P#_5?>6iooO|Apdd8_f6=n$+8*+FLv^P)BE_}-D?%o z83G9XuNJ>iSws;Pl_7WMLHIfkjKFv)a@DP zCTg;<(}2gopRBT-;${#`nld`ruu7liz+}^FT<*M$y>YQ?(n>doPg6Umt#mKzeg-fo zT1A@FzD!A1BTT`WF?W^eeDVF5;66k$z!JZ7|C!$&fdn zJ{os`o%rRa#JS`r>vo6e-G*YQwRF)+qY$1l^aDS`Mu9RkS-Q+=uE0|)judNzDwh}2 zt~@|3=Sz0x7u!N?Sv``VT)d7X28SdjxBky<`g&uwJnHu$k~Z5^xShEWX5c6C>1MnT zC5-`;?iW}>Tdb9HZU~UvJBl(f<=f9U$Eo&ai$aPr0N*2Y&WuCGpB=4Vo9G_fuV4F= zm;C1MF=RX6SE)ReRQ9^;SoXxUNHpOt*#(XT$0HZccdwE;SQU9nX3S@bat4X0OvCaG zO|97~C{Lm-4hGT}%x@kQ#QDq>Y?ucRJSCqRq31Xf%`n%?pQX zq&6(Jy`QI1i-~!|mgqm<^EXmdmK)p-{n?%FeYw$9B-HLi2&{CwVjO`aIt{RUyPq9& z*J>kpuF0L}z}OoVcH#Q6RIlG-@NnZ7Y~*FNEz#--9{I(@Euidw}0Kac~KUF@d{q2eV{C^Rpt;SPIO zFCPH)h>oOJKbNj;-z}F4nIF)QZ0 z@skPjzNBT-(Q$mbH^TulNPcA;94h)E{msqImseJ_3VxO<;Y$JpO1H(R2>^942EuSk zk+%!t;K(Wqqb2JX!;Pw9b2tOieZXyB-GV zwvuD+8;$Jywgaa9#oB}_%E{V{R*=lPBKT0S4gjy|Nezw0mbV~d8v4mJJOnm>W^<_0 zDjWct`?iwq$J-RV=3T!=sK~^eS<9Za_K&}wp7Y#69g*?x;yx6$o3w~GvW0vjzk@?A zPU1V4aI~uBgTZGz+O@ke?|Idlm+o1rsPa%u<=hNN0nE(IG+w-L3@Di|U;G6<$er_Q zIHA7;!T(j$rT?P5HSjtEeN@T{ww_ZKpBQl;J>3P=!8kot;^oP3dx$~uVIcJVc(FzA z(OAepYKr4Q7G&&^u=Nmtt}>NZmzRZ44@Vpsdv;Bvvizn8?@{rGvSY3+FV8sbG>Lg2 z4V^OBN(+DU@T6st6wvw_0A#yAiLaV;)l0@VBBwV_3`j%zC`t_I7#N1*t4#NRr$w&M znRRLTv)QRo^LaQv0VU;}&sc+Hw5aD9pxS9!SQO*0_7Py-7j`9qqiF)eAx2%;m zq>$62E!e#+>^%>nK$Xuuu6~ z30Kcs>xD6e%gqfwXgJtwo);+RSvuyL^iDX&9J!yACN@U zN6nAdeg_|x%=fxb5lE1PkJ||Si;5^Nkn>aaEy`;cxDUUexagj`4!0o^tX%h2w82JT zZz^d@p4To-a`+Wi|MKB;8d?bV0pRB}FWL$;?D4Q*-t!Ou^4fo8C^FlqSn1WiC|10K zfzgS4?|Q|!TKKN7uzUG=bX#6Muobklv>O*qg_egPUwe6H+W9J$*1$Tt-}L%so%}{4#**)*t3IT$n%g zbf~LHZuhKcQ>{s_TIea!6b6--IiT1-4X*l!k#(vhh?7KPQlrj}w`S_1&9V-;`+2$-0#R^NP*Uq@?Y`-f;M0wZjHPFE{SVed4zm44>i%TwivR_ozf%kB-+CBXKgu(lRlG<4)a+Z)$ zHHeC{TqO?ss@x!+dB!^KTIIG$d@#S^-PfuY(E|MlT45S{wpqxuT%C0?z(8xkDH zt}p6*;X692IV;tl#81I1-*PblMt+?jBxGVfh412bWC$^bsLOVWP-1d&gYD7p4r#+j;7n%||t6o#8yH$+59~ zh?3fE?_CMErZTQ9IV`wwF|1bAK2byuBz`87x&-&{|A1Iews>EC_@UkJvFHc51M4*> z-X|MAcw)Z^!CB%JD}9WBdEqo9Pc{esb<)WnqE(_G&S( zNDhtH3H#lZkT6*UM1%e236|K^eu#7US0JYV5ML-DWjCy>9uN_6rMv&a5pcE91jokf zD>?F}a|Ew;zYhBjIPK7yOOM>H7FV?N(7on9x&Qt@P8)bzBkjT7y$F`(b zg+{R5+|H&dyMg;btxOmn?C-Ce`1pU0WNxI;=cR_j(BcJ3*)40FMH>uhF-+5^rk&>Ye6!_GDGctTPm~g^cYBKtnkD zM*>X2!{sKef=;FYUhoDu#JRoCe#hrjA%NMFl9Q7Y5)zW?9?aAo07RfsFd_s}M!M02 zV^gSIbUYlwG2^+zyV@ZG;#=1fm>iKz&}VR!#>OVbCMD@TzQ|E(+NuHiJ@oo`3@qbn zC~57CbHn!C2gJk!$ztB!6&9#p=hfD8=nf`)v@`<@=_2n}H$jXGv{G%2rhpTs66yIm z`xDrf81H2i4MrvhikDGX9E0r{#3)~kc7@UB`_0=K-`h|@V_t4fd`6bHb&Rd}%K!_e z06{EvBw24~^H);k&<&$WISR0UE?wSsSEzen@IxOnV8`KoFg2ZEtm z%O;Qr-N#LfelGuZ_k3r7LUjL;WmAuZr$U--+wP*7Y`CT9;jRx}sa}&H^&;m^9hhj4 zOE(H?*xA`hNPMgbk@vrjI`=-%iYo3t)<_H-aL zB2U`E?^pgf6ldVwo1eLZDlsjs5da_$58qQA6BE6>N^_lo!A?J2?(N~tc+czGw@8V_ zL`ChlhGWwW5>irL4ou$>$@*jBp)K+xC6f+?0hVs2$DaI|V-7~?lRQ;ii`;B0-ULm= zCq>67{ai7F+#n+`If@;(AJZc>v=k6Du)`lZBho5_GG$Ydu8Ku^%7=Q~%vB7M%BYWT z&FhJ2GI-ILPq**8U!v!ZULZHuU@X?i8(2#pKuYj+p&Y2ZcSYaRl0rfLeSQYH`X%y+ znD}@*<=OanGd2#^=|YnWTZ|SE%@NC(hWUe_xXyJilYI)&!vbJzex|PRmIFrR6xY_H ziRouPA3DOwjHfE|7Cx9r1&kNcH|!~;3Ob3NFGtrqA>gDhTwPC!1?D%s+KC!(e){yu z2zpA7t<5LeJP)YS3Lbtj?mfX{Nq7H+X_Nm~wQOOK=9V>Iyf}B>N_hk@wGVf1ZQUo8J+C3>A^4ikFut1gM<%3`R^CCv zw^9H{+AE)SWb=L!&y>6@BEiHTbA2sm3N#lFv2seJX+WPK%<2^#hv~JWYtOi3cg|i|u=kVDP*Tf1HmMg)q=B zG}|DX&-N9^(^$IO5Db`()L@KU-f>d&-q9fjYzXI=KWicnDrYh1FmH zrR(?6#OYcH@^YpQZ0}lr&X$AqR=lwK@k4G?zC!J*rAk%zL!dl=~-F( z;3MFa;#~XY%R3g48{^3jn)#o7hNDgQfP&X1r=6V5gS~u z3o9hXj^$JGX`tH{tM1_ljz+Gpe}WF)Gj8sEKR`pJ3#c04IKT?EdUHa&K;Ry++)XC% zrt$5MaA1!ad`C<4RlPLdP`WSQC*=?g(&HOwUy$%2zB<$Zf>z}wYcLHhXG;fiR9=&# zm8_i0g+;EOhJf58o(ll_(Rt50vm&aC9ZdV98AD1 z0%2qMIb-fnyS$J!A1qHENgG11RKmG0p?KXX2^7XL$J&XMc8RKr58)1UkHmBjEU$mO zc8q^l{NA|tgJYC65}!7X*Tg2Xz>*ANT5&u81IZep9vezg_qbO1MCA6@h%N=dG>U~0cX%!mbjF{yw2_+dYoHiqa)LyKZ+Z>s=}#7{{nsMWV@ts)L!#d_mP zs5}lASK0Ir4i2Dl%)!GTz1%bFG~#hE4@}HVviRBB3RFm2f1)E^=&&?o?H(3Z9q>zt zJf+j$h_ZNAeFm?aAQgA*TKJ)nH)k}stdAVmf3rPR*$J|#nm4mljV6GbS=(;db|jHW z;x*Y()TGA5!fi%wH(<$7Sz0n2aLT9)ILR)6R6^^Lkq?*~-rIf(J+~03poU;!p3oDF*1a8$aHhijGQ(N{Na}jitDMUmd{q9bu20HYYPiR^maK*7Ogq-g57&OB3zzQTo=_ z)>`{1yF#^2S~j-V0M`S29Qu=TbBP~382=muE}O}6lQJW1e*5vh)l=0gAjLXuO1K?MIrqfI<~SQ&otvmVq>7 zfU6Aj_lJd*7y_7bwLb}m{AoF`(W2xPfq?aBaz|d)naL?A*eclCcGQIiKH@Vqwe1}L zY%@_~7*096LYBtN)ZN{UxIFnal;ImQioSrsN#cXZ%8uKSPE?o$;8F-WN7#h?;V!>5 zbKZYMJm8_ZNqb5i?8%?T!AJs~`$F})VSiz!$4Er&hlB*v&=>nBjg3LfpBmDXbQr5o zZw$ooxkBxiCY_EERce1)BYfcK?(KzmLm3U{hk}bklB;Cv3y0o`_{^I zaaE%@&-2h6jC)dy`MqjM8yTbIv^XY$Uig|8h(-Vq>fm*Hr0qr|)Qe~bxG1=wn`iP2 zwA0Hxh>Yha4tGS;EYj8NCim`%4qA6|KHU zD-P{lQ7@1pz`LDI+JtBcrDQx7xB9Mp(J_8 zj*>Pvfda^-u3bAx1rWFWbhBZ63b##^@tr?EyjC@o7G+Yk6FtV{BOBq)*cF+fPNVU140TG&Z3vn=iKNKc4sJMAEs<6BVE4-N&wI0={dV%wu9L-Pr+IaAoX{awLua(P8mB?oBR>l=^Q_^dS zMMR(9+1oF)yiqUTXCsrscxLtxTFZ{=%^NACXREfy=P)kBVo}uz{|(-U>)-b&R|eWr zAWG#EBQ`fr5sGqMjyLw|?-|Gq2YMlW&!>Xum;Rs-Hb-K*B4ZTIsrMBr%z_iw^i+>9 zZ58Sh)tpI>H&W8u?Ij5a*oM-Fvy9S*k2G1q)PIjDInWuPc~YXzqaE;TrRjl|GSTez zX5i|je9Hrlor85rZIPvxu(t>3artntyt*p7KWP?)(b^oWy*?ulAV2iGq1d7IO4znC4K6u45nLTv`V62sypdfzJ&*jMrsPBl zK)sPeXf)Po#MiZ4B^{}zcfubfjBX`x?K=xdX@t*5H7+UtR?WS7HJ?XtV)#;+cK|MY zPar&gO^#;t4q-HsC+y;dUUHys#*}C|Jzk>PBh9Da^CDtqWJd`~zEv02ktKynYluzE z6A~#vXug(AManNfpPKEj2l1T5*OyEsT@aoTdP^Fv94I~Q)INZe!1uNa@9(yPQ0kwj zi_`n8VC(KB(a{XqsQcCF`5!L0UH4bk0wxFZPO3zn{l-w{V?3Si5cmvm% zS=k2)ECz-I)gt1nKqDXtL|0f+gzkDkV4e*&w_u6haQ)7c$@EMhN3ZImdXIkcHDfNz z!}W%p`jPppKYz~mDEFXdK~L>on{W|soQNQpTH=!c$sJe+U2oMK2j8J~kP*-M$39!a zCJE^>iS`?}GlJ!&Qq}n~6fpzsJN z+jxM`3?+iDtOo1J-_`?@u<}hT?PWrK0?jcZpJ-S~X5z8tIU%Q*iI3M~(=R2^mcPOr z{69-G%r@^>paKO{oJPE6a49KJ<~P97|%Dea4MK#JbzAgWPGi2yk;K|y<80g(MxR^ z`X_qNY4-KTE^^iopFV~7Ms#(_b2K@jx3?NrH)tB{xUL5D#4lgI^zxEu^~Xu#ePht# zg<4p6<&6i306^?Gy!)qy#b6dy`W6KVN-&nHBK= z7g2000}%zxx|-FDxqjz^MN<5lrcVmmi7Dv+K<0pg*?%uJ%JSdX-A}dat>;-rh?NhM z)@EUsX{Yjd^3|Z1a9e(3W+L5I1Gk92kZAdUDueGLXNaeb+NDH*nvA4Da%^ZRWBizx zh_U!Q8J)(18dAf+gXVUt5jO2F|JAbVvD%({06xy25U_4x#WT!@@L1S9vYuPoWE*pjRr;s0WZ6pU1n#v8!{o+30a`{%**jePYp$}Y;)9?k zC2RtTWB+~v2>;5t_5&ZJGUQg9EJH2qw3XM}@0-l|3RsW1S8;JUSFQedbW-2;>!A>C zWv9_b&Kw{0I$`0pFH-rY;_m*0oL677TYQ<0~m+d(UeazdFa6GQylw zedN(?UEaY|(j+D$0jSkaJ+S|N^Tkt1OJN##9yigJyEWY25>aiBB;3zD4vlihydNNJ zB4LjoIr7Eh6K=*nuAy-?sbP%GJFbaNb+M;c3r7$mc8UM$5G4NPMX8{#wJT+kvhjf5 z%kxJZ%6;%VGuTrT6ne{Rs;)|#K3AL-cGM$8yGHXv zknL`W_y$TCJ!Mk|cBy8@T<))_N5Axk?;WAW+3UPqB{;OxQqgD%M{m;^CW`t^9V6B|Wl*aUaQ~yR;Ua1|iNhujh!r$Yrnk;b% zznj||GDp>{E&jis@kYpZS%g=LY}k)tYZfbEESJH~k(zu)HPFdzt(0+>8+=LLiP!vN zXHOj+)$xf;tUP(il+h(mA55540_+t^P11%327hoHn2NiFzJGlC-$DNvrXVxN<>4wC z8#l+(sM@sPjpA}L|A;~Lj-~UN&?Ngp7*L9=0)wWFoOl7SgF7iUEuHS#c zPWmBgO%+&4Q~r8YnEZsR44*kVATY*U8mbV| zLIQA{OYpkD@2@b7QU5XTfA4>TjM0NmCNIe5^+TJ%u(e*&Ggs++oJQ$NBYMT&lgz%L zsXZHA_E;%e$d`bwL%C^p=%ZKecGHuBJ#rP@VA$w@D=GfmKG~mnUoojJmO45H~ff- z!s|NA&HYimf6}hO|Njgi@H5{H6*~SHnM*U){r0aGAoW9En=7=FRs_ni!+7fg7mUhn zdM)c}>5*y;Y<1(_G1+>C;-I#uxccksye4ZRwwuHCte}?QJ`!ixb}2>H6972>Twe$l zt6H*=Wq~pLL{qt0x~hZW=U73W>S>GXq%5FWS)(ZRjSD zL+leS^;K(WyaH$`>Q(l*8zc!yrHqw`F{2SjLql$bEwVO!#nHMA1zDO=8ciFv&;NH@ z;DaLH1-QcRnYQ$D>Vdn@@5UY7>EEQz4l9*#-DF%Q+ZpBI1_{x}npL*sZJhnl@8Q4! zHHuFtvPqLz=ov&DNsQtJmU;SVmsWb`E^)5^SN}guO=g3Dh=&+#N+fz)4OqKX#x$PP zI$+Am5%-vesF$sPGCEKmdCi8V%%64q1(oCTlAyxd$_m{6aK-aG?{YK4+hShg?6;-8 z>}AJiKKRc|$6}6Adk38o*+iDD6hNC3P0W)c8^`*Tz~}$8=lx}3n%C7Dax zi(GAd_klw^AmVB<{uN(RwP+UNF(c@0PxP9U!!8EISG)VN@`n06Bh~yDcdrZj{^v0_ zV((5+o6no_Rz0hReytJuV;NHtI0pr4%F`nfxZsyy_Nv{5*!@ zGHOB0zukFJT;TC2M5*zzE^L42T#)-f705ki3*O z5JE>Zkcs!B%^#XOSTw4DzePthUlDyh)(1q79RFYoe?RONGf}COJS9envbV~IJPQx) zWWu;Q{Nke`=1HeJzW9!qr;$;E*IJyJNsp>^UuQ!aQ@MPYo$jcQhUxz z<3P7*$@v7deDa4SSY`EYyL6sl%U#?w{Ip_Y=qR!POhyz(F_kZ}T8lif^vZun;Qz7t z>$ueCUKC}Gl6`-|n&jZ(=SfE?w?S;(Fz_AYi&PihKY>kgo_#Bk4^}{pgpR;wBe~|8d)+e!prm$PlIc_q6E*a^o(x(Kc;gV zpFrx|DJ~1U3^~&^g<@<|Ku+O~U>SePqy~Pc+XQ>vw%Heqw>WrS z-v^o{D$cREKn!7T3|U+W!&eQ-qezS+Y~>*N-#7v*0Zd6;w&|E+cC|Od_Ywc*Qs*18 zBtL(ke0B0%w%u+-gS+OPK;|D~qogv=c;3BmAMF^c2raNKZ)VJ>dOt!#05znGA^yR_ zRS@%120C|E{@<-?zwCc0JnMQJx9rKNmn_loobbecysE|;)W(5UiU7*6>6;XA-?t*mk>#&CJ1J14r)|)P>6e&AgJ>!4 zwfBRskI++-4;tI`yFy+X4*H|Yy09+-WFTP}-q3>6uYVryYsC9W0(k!!PG4Mjl&yWaRwtot`c)Ua?XY^JUFyeZe+U#A%z+dc)o9qqvqnWxsc zIy~7G{Q5HMFoaZwWNzFtq9rYAGN4R5} zh-K_!2>N>X7Vr`Fin8IBOm`I_xFJ7h+Z`FNGTQIe4Bsn6`b2Mdx1Ra-@9JB_EACH850&*qM(6}j~-z#uD1ET z+F^Wf4eSmupJf^^L|!WLqHnZOUz8tLg!ad03!v}T9rodDx~y>NY-KPp_x2C}L`c;u z3}o=7W{JW}*Z({MzZ}z(x(xKl{U;q5iQc>Eca8;ve=j;be1^qipH0uu;SAY2`CfBO z$i3oF>EV!>NWMz|W}ltgNd-^21U#5>xx~}Q6^h*I0a0mg86&b|ebSMIKPklTe_Xa7 zjQWAjctc}e=fTZH=K?-9AvFjgXAw?T;`5|wDS0ww^w``I^lc2Bs=l9yVCMc^Isi`W z^hMs-fz;n&54?kLB2a+P?lfFG&ZPryrG!BfCx_9L}-oj6&yBXAf`nsV3AC$7mx7hvlNdlH&M? zOKmJZ@r+G+BDxyEBY1J+<(=gM;1uWDz9Kf$7tcxmcdNvj<59B-mkoZhTK|(R2oCb_ zHa*(tJ{WJPc;~jD1^l5*Bj~ivKg_3Jc#H@ExsA|{`px)pi^wipu%EOpKy>nVR_gxm z1Fk(*Q)fGs`B<5bKe&0=5Bzr3q{|9vQX0(a9BaeVPLcB4xpKzXcU^FIL*d>Wnv#%G zjFcGx`CZd9lH=x@;EM;<@V$*YbpN~l6$E`HTSXLDt_L?xSUogM7#qMFee&Awy@hzRcTj<7i0Rq520swpWZ|Dn{BhR9Iw{HiKxw^XvRtF&;C-WX6FSqK$jbBx6Gs4!i&4+JR{EpF z+r;$`O6ySK1dpDA!!jFh8xB^Xlj2U}?>&!C;Mz|eNG}8~9Kd4u_s)R+ja2{{5i$H` z6}h1`FsTb&<|$41KfNA9@-RLFOHih^KX+7Y^{=z=Onpgc@w=%i?(DIG?O$KPHA>_( z|Cp_i2mUHdJGKP&KYorA6mlFw{i$RQp&EKH8&f?pS@_N6Q(ymPu9;+4{D}?x5TrWxT4Lb40-hLy@Xxm9>=DP} z{Bp_&W%V00A{30gy9CmPI6qKB_etCB#S^yBP0Gi>OQb>nGiLZ*Oaxx{dyO;sXn_z* ztuxU7$uOub-&+a}7@lg(Ts?`wXU=}$}LFW>u44Hg)k zEPJf&8QXy;&R(+we8=ZM zxBl>5c816toX(&iwB)jZ;-$$^mZ`zA9XG4035a-^yr)7+xL{Lf{SJ@%=WY&o=+e7T z0nqsTSRx?ez#0)(Whw0r;RrVBTkz9``3Y2ME$1;Ml-dUvG%p%F{of5;iklHJ^tOG- zpy*_>mM0PBrwe>VmvyT(@ADSW3E2*ONoRSDeT1$1mW=1?xU7wOcI5l*lw+OqVcX?x zGP)UXPzWga@f}8I7NH#?;_xnG7SPe{-|H6VUC;xfB|SodPV%*FpJadNafz}e9rtB+ z<^*UdgO9sIO(*cY8-O=BRMU|t-w(?F%z>q2s(cMq7X}PKLU+py^skh1QozgndtZ1|-=SkOBVrtCM* z1bXnRzGfbtdpsz{n}fAt#f?%mO#p32q8C*|*KQ_H0@IEq1p&Ris|(~E^?o(o6atYC zX`7RxZ1(tbO~v$uWlG5?n4g9oHix~hs~+&(=*5+Sz2-($|G$Wm9sm{r1l0 zl>(h3=0A>{cTnC!WpZ6oXHpn)iiGS!ruJ7>Y1E~$5InnAR z7e7A(b7AhnNn6`5z%a8@fCDUxb!j1G*#9?Y-iv_EuG1V)q2fpOrk1kmX%-w9UzxK^@j7fj-17MhGom5^BQ<)mvf%SA`VHhsYv~^ge zBqM3FI4wg{_E64mIL=IbPD)ElSzaDqcd)a$N$#@0IYpqJB?~k>f*tLAW1{pzLRM21 zTDNce`9@@C6M{l`Oe~WJVLdlDHrDp{*$E^8`OBJ~Z9B&7eOV{Kn3w&$%Ci5FS;svb zER!pBc_ray%YI8_IB4%yees{OnRaR2T;%z-7yuSc8HouHljaojWvCCau%2C@p!u0)03KtV*G!98hRuj94%zq^ z1rWrh&CCeL<6>cLkF4D_#vPtc5p=TLne$-x+M~301_dc?QbR@;Jua-&GmuN{4!d0$uX@>p0AwiZ?>O zJT}LkFAKAN_m2#iS-(Law zL{q7uP+NyAp`@mk$h)P()&7QxiVDNSeuav^*lCfQ5d-2@3H@i-*Bl%iR8s|i z4Lc#mk9S^SQiH9$y=RSx|GPp?63jYPK(F(U_}m_@S9fp@=gAJ3|Gpjv`%88F8{glN zzk6(B-y;oz{M^+Sxa5-IOvgN)9;sV0aDDdL`w6#iQ|og=Vu@*BHnD-WB|qhT;nH=Ps zzK+f)9Zk)~cfZoNeGsETHq*xhVxi~)VQ?*o#fYC#nL%luY?vDv8G+U?Sy@>(e));Z zNFZ+OhExey&=6$s`*?Z}4qfsRLA`PJVR?!(v7KD+Uyjvgj0lRmz672DiZ-UG- zT6dR9wT8y9gMcf5d0g83-QypFstU)XRiY~fg#}b?Kmn}|p^vZ#TerNDjZt@3_RDrX z3&>PHlLh$f-}r(q+n@A1+xeWdeP*QPF+mY8<2Hns$gTxH_z@FOZ@#NW-OpXCg;wcFE`mcLQL!-b#6q1e`r7Rt+h!~{8}S^ zK2tukI&X7?6|i2E53~uJYBs~42rARRW8al zH&5Ap0bpza7{mZzlbK0BQ1l=wabX_tEZk?gMt8abLm*-#r-@scJ}7C)GPu59IULsdIt`v z>DR9J^%ZY!8px=FvLpq(7rn9C&YFzFhS)u@p zkeq^U>HKDU$SII;1_kC*l9RJu&3yXw6FfeIe2WYwv|H~=H$klMbJDNfDIywny%kQ(E-RMES%Zssa_Ih?~15~3w3 z4@Olg;P^fL$jD%cam#Fukh9%vzB+1a+J#CqMN>^p=PWo9G?lW4SKX(k167je$4vov zRBWuQ4kDm3HWutZ&|{ay@cOHR)zt288{dfCU7*lk0t!Nx&6D%4NHY?S)dpN(ra(s) zqqTu8(0N0q53JpCtHF;z@sIZ76>dkiC3<{_p~VVN-+uN4knh??Qx-?Rl}o7x)0Ce% zu7mzTax?CeZEd5!hcc=QOungU9qle=d!FOwxc)Q>8zhSa8r zr9(k=w9D$k4_eTl)_DWiAzN&1Qqnb-?ThoaperEJQ|KfdF4oNgMjHG#C)C+iX$|NC z6L^Vo|LyOxs?@VRd#;);ifB&z@k4@(3zjNW3zmkR`&`S_B@&DW+|%cU#s~?=vjr|q`1T*B9k)ce~(+Xn5@naKv1+SPtC4#WE27pK^8 zx%cl6YV4+fny>(@iRvqir{1C?PI@xx>;_$Dr)$-^x>ImQViFQ6F|RW)xNgUrGhi`w zZ{+1zTwI=?flX~F3W7T1HVO+%4g+tyx0sX9e(sx02*Wl-0{@$f5yM2xyD!d;n||84 z;8HIywO?oz@n0g1E7*P#2?@6j|GfK*CSoIL}&n-_RW@?H1Pj&gMqd7t5X$^0=~03^Y9u+lOtI3&bn z>uhZ(BkS3-I$)~M!Gb$dh0iv|lO8f#&sJAQ;(qj-fPkRFtaHBeb1J%#6xcpyW?8q5 zNk-5c1Vr~XHl!dbsJ4~ix7BRX$g3&kxS$YCiHV5FW>T1M)&{cFG1LQXQ6~9~iL!uK zD1RReJVu5zd1Yl_mqP>dVZej{(;!v$dKW|`oQ?C-eNT=Uzj=x7=o;#F= z0MRI~x%~XqrQ+qmU{gVXO>idAJR3Rd8i0>845%y3Mu>%D;88wK`Vbh zzwtt%sb-V~aFQ_|)N0RQZW4lQq)@L16TLJ)g)=^Lg?rAcF3<-z%6%5oU9xiUQ0WFy z%c~oXMjaTIr`@JkiEG;K%R!x7&g4#pbaY<~l;=Nuim>Ss-3rb-mWwHv8irfO6sU*r zK0(d6X-$7-H?J-{4Y81dKzQ!4y-O`=iNE3Z?HdD_ySEc|1cc#?43y!4`9^svTcDW9 z)XdD(EQ`(i8Did>5Q_JDwjOb^r%`Vkkvx(&NGa^vhAQ)BD=fS`o~8mnJcjirU!^zk zcz#~e8@Wi%8E)RWfz{T$({SC)?)&5`kFva35gr&JA=lNUN5cB&AjI~xXXgUM?z`o@ zix2)|aqIbiu((!A62bP$K_ z%|6B+;Y}B_2YYFz=5>}9IVEM&+eHOdVvWYlBJI~;?($S9h%S$h5XkYzl>YK`4(G>N zJIz8RwI@9HaKeDNX+DOWZt?AMPwce$|Hsu=Kt;JlZI2)!t%3+hNl7Ci5<{t^bUCC* zhkzhCgrp)$O2>eRbPGs>NXIB0GJr4)49yVI|55Mv-}`;D-o;|EW|;H7=j^?o{p`xw zmiC|e;uT^CYR$?_8{j)1yiKVt%KyHs_LbSM&~sM5gi`(C#p9!Jumv_ye>dT~5_D^8Yd}%VrHb)dt1-Oo3^-mlu(cJ0 zEMmax_+_{vxTdEYnvbyFDk`9WLOq(f*n}2;QUlWQ?q67>-_w8aCvP?MlsFU^4C>=pPHI-`QaV& z|^uUxo~pFC`PHdUy9vv93{`LJLBZAjPbJG{R7J<18Sa0 zmow6wh8;IjFYhHvAgkRDrQcKD3H@QQZn2B%++`1ARI5K<83^gd-&C(7!V+t?Gc*#3 zGuiNr6>u`TOf}VxUg7z?vm3!h?vmd$zc?@wpbXb-wDQVXO721YQGCdP2XGiKpZR5gk9RYAWjMQ`OVPu3fuE zMnM5O+c}&X9#2+Xvj9n5-GRMDg*?C{Yr|knWEvh`2VT#g8&iIE$800cPjW4p!*6Ux z{!cEz>e;ESfp~sh-Lp;9I!L93EPea7972RtcHz8E1`Rm*IVbOFLII(sUSyW2M5f-2 z($dxjO7za#k}pGJ`>|)0pPqE=j25N!_NsDmavB>=5fZ&rIYl+)sjK&Ocf8p4{<(t& z%M>949=JX^d39JA0UKKoxJx7d=4j>yvqo=quQauB6U3dus3MQyX)WLavG;74bz>p|d#w8;tn3OzS%RD@j1?mIT)yX+HIxH<0 zz~0`w$5v^B`t@sZIFElUIw1kgAYNKhT1xzq?SrNY7QCUPWb@8bqXswh&X+GBz4*yY zeZi+sUsG}A_4QvYBH4(8S-|y*K~mE9==i0oT4c-dhG+_`zPfsSY2f9-rYsjZD_uqg zbZ@UZPm{SQ{}uxiQ^T``vx$jcK#Fs|FK^5k*E+!vX{&+f%?scoKze)PlU`D8<{|

vd&Qt3?td;l;|C3)2Y;9d0JOE><7}?{MiSVw1 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 XJ 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%0A?PE7K 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 literal 0 HcmV?d00001 diff --git a/docs/guide/images/register-name.png b/docs/guide/images/register-name.png new file mode 100644 index 0000000000000000000000000000000000000000..94584f54df53d5d03b5d062d3118ee5837ca5d9a GIT binary patch literal 232481 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�(boENrI^PKT4{B=JE!9 zyPIrg7b!~6_s1LDUMcK045+M(P&HQH^;VHSNdB_(n#?IiC%ljw#*4m zLP|=?&%y$}CJzsj2A8Vm6MnKWECgp;duQVXMv9dJJ@-zqemh*bS6{X1s5}_jJY)`o zzl^WFH+PmDdGMC(n^fd9hau~R&Fs^hEEOD29I95YtC>Qpv5!t_Wfm)5Xj8~xa*e;g(UuAGQH`Dj7otWrUnaj#n2zjAN|+* zL2?OH2s1U13jwZ(z~$kdX=#1K)258j+SOSXpZ~+xmxn{ywsGqb9$Sh^B4pnpB#{}E zFp_=jOUafsL}qNEvXn4(F(bRNZwW&wgfR9cTlVbBFt+HsCcSUH-*+6JKi)r{=Y8hB z?`t{F-*R5psXqfAR?QPE_L0s`zXezNZvGqrP$LVOFcK2@qoe7!Ip>R`PLX zG1U`_0?t$(%kh?t&eUz^8OaPNA(Tv4GqX%mTJ|mbco`P^W$z}Z!#g0-1O_SSh?th= ztUk!5Vqm^xd1BE8cJ3x`a`0qBZu?`eiWbbu% zT>p}8>wf-<-NWfqwVxb>u*>Gx-wu)ea=tm&tK8<9k5vZL@;p}{n3vzDgyw) zb?T^aBXuOSIYfOU)2dchS`t6PIB-qI3Xz$$@#AV=H2IT|Vnw!2&UyaeMaC^&kGcV; z*-VH#jKdtR*{F}rQeiaT7&;DztSNQlukB1v`odFg44gYbYsCa$hXdp62l5klHKF`z zDjfbV_Y1jZGai}H(5Ovz7*8yJi!0I+jk#`S>4t>Es1%)`zZwRSQ*b9>h6H z`pHT?=vJzDks6cCX1%IZ#&p+tr^eonfwvWpA9!Tf`ZA5{bG(&e+H$ax4J8)|CA>fA ze+s&(schjl0Ut7#m@d8joKD>FW8*~w&wg#1_OcwC0^_AmGYh;=lt_NgX27pitw6!U z#Z5D(y5S?J9LI~iISV-h?Q8l24u& z_~fbEirH{JJ)^qU{{k%fGWqFv3fhdh^o+aG_uM(f zq_;~i#+&=B-@^DG?)FAxw*%!Z8{-G47pr%B_0}RuMiJyt$7-iP?_+Qo%;^FYS*H%{ z@lU~{IuZ{z0_7=6{DZrToGVk@PU)DN|14j3{mVGsO8_!pQUx%-#u;1S9T<1K=EJ^_9iLL(xBHvXJCrYmg z|JE7*)&Y+LAZ)z8>JsOdi?bW8?i(2QVUkPUK9IX3{C#+JHn!!?txh4_d&_iX`YjbU z>kzPO;x2d&PJj^8u{e(3J^h;}Z50IaETCqZbe8q0qEN$W73;rpx2H(--PIK54M zH64!q2jr#X0BGOXfofkq7KapDvyA^}7-}^25kHIm#*#T`zTR)8#c%FQr8&=Hx(ccH zrri41<%)WczDV4|?8lKODR>U3aij^Zp~%&tlMvePYJIocfSfKl#z}*Jrhk zBrd-{;@*w9yfD!PM*j88zg#)s5ExJ$R_h3_s8-)&3dBq^_Pq`%hwAy4Flog(cC-0L zNtm_Sr_}WrOVBwx?y*z5V0Lnw=K!C2@(yUKM#bRn?!Lb%^K3uvX3d3GD4P_reD+=Z z^%Y#S?;xOG-v(UgHhrWu$0T;Ado(@B1sI5+ZwYbYQ-qwcmKJAiX=>`->73x!=`5~m z(~+H7uX^OW4&Z=wb9*sTX2XU`Nz%0ELqLoK(Q_cvh#u^x>>plVT`JjMQS=3!>P9x8 zap9~#`y;sAmqx4Hm%q+KCa=02&h?+Fgt=@1IMY*v2&%#!h6W|M<=&8WI;K~VDOR)e_E2%O)9@1Yo8(}i>~FXMEJ}qq)c!-16Sh3 zrVa2c{D#gLQ2K|T`8pNFcrSRiy~Yz(IWp$`^FaExCLLEA`)JK-Cr)Pj5~P~L7*tw) zn*bE3u=bF0KExckYc2W_TKIVNL#5Mj+(EbWxZjr**@U#5obMA|5eX-<^1qMOUiYSK zb9sMo6=;SHZ&kFly9&6KMNEheLt`n)R|`za=ust8O|Gol6!~T~o*z;b>!TwNRKShs z1mK2v04*FZx1Y=aO&0NUDKMU&aRyM7)-TEtdDh)#XLi^nhp{8^t*!)G5qGY_eI zbuG=H#1y;z}z>y=I%yJ#9*+~f1p4(Zf~b6D=c7Ym4Z1e} z<~~v7viIWhX=k9-JY%-l=%`uEO4|iK&;bz4_*1&)f;o6cD>gFkEcOR`F1G470kvMx zcfTW9W)Zw5UmD4T>%l{>A2vjzz3cv2UL;91ly$|$Yd-?WZ9Ar!b1s>_D+Y#=%h(T1 zEtv`zObpOPp@d|2l2-u=FED<@p%1j8UQ_MTRW5TK+tnToPK*6T*}Vk&(P};M*|tug zU{6j?ev{e<((%Lbnq7>)OArRJy}`$MwQq;3~47`=4v2)|U^6quF5SWz0J;U*a_}P~Qtc1=P^!ctW^=@n7w)Sp23K#Ew`J ze`^R9-|GJAobOMFNj?yC0fuH%c5U;I@U!``^KIe|a6f=09n8_64vXdk2s{NayW3*# z*?cO1Kn)E?x-kr;>ki%j zK|rgpBfcG@0_c7iNF89$B0-*b!7OIu-z#H_IU^F;Dxk8t(3f-XCXs{d))9Eh*)*X{ z_CE}c&i2_KeXM8=a6dUEJy#lugAEodj?7^}A>j6i2mkP3o^E!B>t~{$!{o5y3}A)T z-%+;-8yiW|E_DFf1B}tcW<2{2NG2fCYd4Ds>@MULX+oyx@C3kPip_WK>mNV#-s~#E z?R^D60|qgDyELGSzjlAM_Il-Tg&BAsP*d>}7{fqVrN!)8Z0@fDaGWBW`2*+=hX9(E z$*;Z9X25`g2S@AGTG_IJwqz*>%mLA5Ftz@x$KrESWXqX&GvyN7=Oo?Kr&F=sI1{jI zHuojhgZ*uX%EdyZ@`n>?woMTh)s9AMwV&C1uXqPb+4m0mZjXYa&K!DZpS}5YJj5Vf zIh<6UviXBRDne--zrqe`U46gRdE;3x7bh6$%SBU*2aw?+A8?r^CRj{Y0?C#Y9DIBCb9d zdAEK)U8PJV$PomsJ$}r~3)^kY!OFEe-M39SzJoqjsx!0J3P2^$xHjQc;%7fzx3otc z2Yya|w_ys1y4-#bfCFU7As{dBKTXcC1d7m15577S=4z)?8i)u9?Eyj=2-O0v{ZPTb z!vA_Nm3)@ES#)->Zq?m4HT{*`hWyW6O{3BH6L^9$bnS!2_2q`Mz}8*_)f>A3QQR07 zc|UNv-jTzO1lBh~KL-WZ3spX{-gP4)xfX!wX8O|&xOTZ)3bzyEZ?+{)plenfJ5v?c z9)blgJ@&{QqpP{-zVKjR+>6`W4g}$0hPt~a9}fpnt;Yri2KFKh_NGE46ahZt@ZUVO z4*!6Pj&=uCz2$MWBS+INV8JEHC9W&co_<`@m?J9onfp~#f4Wb@haE*_hpJ>&tAbL~ z_>Uf1Qi{U=o-AQX<@eH@J$S?miJ*0OGKpqrXnWgQji*!4jhN6SxL_ye7Xz|4^V_kS zqUIH=_Nu$|V^ z9@!Ubfcr5?o3j1&_76a%#!6}<5;y8Ie*Wu3=?)rY21McN%VF zWa86v*P6jhLokigMIkrcq?FU&@O zgVa=xCw(Z0LUjWde83#B8)6P=6kG1Z?K*u5b~r091^m$wfI02PFK$%EK*&4ff9`<} znk`)i5g2E)nlb;?pGndV`ZkX!z!5AxI8_MW4mp`O?fi zOd>UMYe@#topJwg-{Qes1t<$)8uK~;=AzI(>k&-hWQ2>Jj^cji&i}wBcCAWrA~`(6 ziavyQ{19;Yah$-as)uKN#>hH8yW(LE8u{y zOj5MUY?K<>Q1P#?gL)6NpIaMy&8n!M6hHsm;Miw%^p*K)hr+1w;Q%V!`a+)fN(q%g zkwFo=_sWS=%O#)LIed5OJM)UZ`gylV#E*=O-1p`n9M-Ik_^;MtXRfhWf_ANez8`cp zzmGvm1GGV^^$tMgKXg7N)FpJ$#=bYF zYc3`_Tm7&Y_yFBADrW+~g2O@XeY{A;A#(Mp=it5I*Uq?mKi-v8I1G#dO{sN;m_gDV zpV?wAKo%~}m!*Ud$7%?J*=QfzNs~KCSMDtW0r5-4&0tDyC(xETWTG-`96H^7d*%K1 z7zq5X9_t@oUz5C9S!gDo0^x-Qjo4fp%kGDRcm@_XVxq126|Xg?PcA8k>%G)P@AWq}?q$6j z88Ka-J))HY-g;G$@T?5oi|@Hg3h|$p`jBL_2}J$?2p zC|`OEf|SERet~YK#-cuWN#~0DudhZ4ldF>o8O&AK&&VzLw)u1TeZM%&F}Z(O^#0)O zqj{jM#2su6JO}OPfLdG)0E=beB^^^!oZULeU2&oobvVVKJ@7BTgEA#_TKd#JfO+z5 zKkpqbNI8TE?C2H`o5zadAwNITOOKwGW83eI4&M5H?{N9`&)vBfVs(a9ZZm+{I}GHG zX1GoPV{4`O(~0NfEBl8y06Ax0yY%Oqfma>vzhvSu@20x^wPLwR%a7E5xT(zulo=0} zBiupG2jWo8c-@clNi@yiKZ9<+5Cerw2Y7iC<8r&MYL7h7cX-j{$$JsQ%bVa$QaUTXcmAD~W; zMSUPvKd8|)>AM|a4GOGinMF-Ytzg>PuHgR*j)QdO|MT9-W6XB^?l=y3tUfZfp1Wg5 zN5XO@L6tiyWT8Kg17ZCH(RMahP_Hgu`Seb4>L^Khw;&?h1MmaXG z!(C}pe>0m^v6^>4AXf=zPuaMB#pQ5_zvqjWH6s~3494Mec`s^{+7{GG$Pixkz?s2b zM2?f6pkw#68$SeF&YF05l#e=5_7tG-NWSIY0>{##;%e*1p?N0HjnG2VA5yUwhuA3f zP6ZD5G(b`JIFg=)&Ib;9p%C5P7R#ZDFPJufnK+Ui+kM2US=026sXk4ru=kU8F<)B) zy`B5Y1ILmt2l#q@IJR%Q)=~Njq(}n`|4Uw8An$)CKNhJ?a>qJc>Q)_$6x=YCcEy7@&C3nARhRgk>{9-QO8BjH=fHF*-wsWy9|qeA!}O@rPjSPuwq1B z`ZTQW6ixHVx85pl*ENinEd-IJ+tcf4gh_MEHhfGO(iPoL>mq*MDU&|Btl|kv(jx)CWIi}!EtG?4F+{^vwBcn;>Hph$Lnv0;?ru{|*rNGoDlrULnCwStk z3$n)j7)@!SUjLSQS{F&HG$adj%<=k~aN&?MS>scRr!0x+NCY5gjVW_JWlKr3wr4K_`c7$U+z zkDLnKNm!7);0?SHNkQO^pk+8rbTi~SX#Rn%H^y6_N3x4J4jo~nG8X_G-f5?2eu>L` z%Ww0bw|D|5S-?mfj-7DQmk_O)Yy|~E$ssf}G@$sn`4Hjm=EhgMz2dz5HFkXTm==5x z(BpfxukL!wE5*EiJq(nJv%s1L>XCE!5wGdm1%#c{I zTsLf+wdI7Vs1(_-x|pjJ{xZDE&L~HSDy#cFPqSySP)^v#x(+yGqVwekf#DZdx4Y4A zGc*ga*<-2Y9$|BkTA(Uf%0E7tqbWKjofDrwUe1xG2U{a+umvX}suo&>-%Wyn1u^5A zcL!SuLky_&Sa=iloqT@io$C}pUZA+C7J!$+c2CfL@(sgrg&{4_bvqpjlozE{t6}Y)pph)e7ieVAvE{3cej_O zIltHvtjjx_r&eqSWfYr$jEQS&Z194cu|;CpT|%O06ar0sCZ0)~K6a|W;hKekdi=9( zFuTM9C7E*lah-3&+D08M8TKR<$D>Z&`)Iq79PQ~?pt=uOcrOFTB&Oc%x1<`vcR^=H zb(SSY;~DOSx|?Lgwk&+wE3uh@`=1(4JP)Enuw8X(y3o>3xZMM`sMsH*uN8Z#ypGv5x~z6kGgdOnj%#zS(>`82%BfEytr zvmu=XRzr$M6AOn1lEar%+RfdaR4HtW_9OAs8DB9ny-U@e^AfwC7&@Q?_4d3X@9mr8 zNJ2OO9WgWEN3VSn#wCM7h%hHUKD9khcnHZ-rcB*1 zs{p19*{=G6Q;?~^Opn2x9xjOD{~0s z>6eLRb$fe2a5r!oc~_!ekOX+>m^&TwxRXKy|_hn#icC^%Yr zq#t{d|ICTPXd>>;EqfXhj`%j~P<3^G@|5TBup7CafN)CsO8N${?)r>)~Tnn{pVO7HUj)??Y{@&%J zpXIDsr{K^nfG&z?-vYl%#asXRe8M7UW1Hj+!H?d7no7BPif;qK3gb*L37ucxa4*b6 z72AaLq6h0L(>R=0(qkV%r3}6zgi`KFc@!|qfBBZ^y;V#X`w~L>^`pOL=LS>NSngZc z1fyKHf@ZdtxFGyYDhZu!A1by_- zfOXmSd!zbbU7z`~rY$^t4B$Z|a$t znV5rJ1uLyNIRq|Vn0Fr7pk77VeRY}2bL7-pO3xQl|2!mB=-Y!@y(tw}*}gfO8y$Qt zGrdXGp<1@QLjj3&*G%KBGO);8&9xEE(rwPy3c>=fPwm%I3&94+bN2>~dFyLHqm)M; znmFYg@8@b8)tWelT&%NXzS8MGPtc?ADTv4wjY4yAaIf?%OjN%?y)9&WQYVUJt$@JI zl8UfEI;v7s00GPB_GL^dRFQYSnj`4&s?4q(Z@pw|;jxXD#_U*i-(5h3|E~c`n7gGf z#IWorDem-EnXf9@oic~!o8Cn;JhF2m4FM;jW_H7u!Qfdye)VC zYir-Vw#!XVdd38P@{Ae8xCdouUeHQZ6c(Tr+$RLk;g4HFNMmo^x;Q}1g6-N>$Y&lq z&&X03g!*Tv}SbBTL;(PykU(fW`XKjQ`)mVNrESvi4*HPatZB+A=Dn_&E$QEC#vTLYP z%ry6N)>>jqNRUfJls;*t2a~GW0FC%s0Yw+D@JE#ehRokp@h)Ae6vkz5TTL8vZ#Qq& zBoWJ~k)&~NPDK;Z87KbxIb@}*a8WC?m`sz$1eISPM`3+t9Z$9K%tM|UeMaeqze%CqS?B@!b6cnod>LZ@h%Zu0 z)9T#`wZJ3n_UmB^7;c!9pHjB2ZwKGB8{aNFxUkXfBb^P(zI~@k5z97sO*2b}B9<8} zqOj)L3l~WGam*ZPlQH));mYFFm+B_t;9D>0ZfEm&{ds+|O&J|^k5IBRh$Lvdq+U02 zhD@DuR`wemniYrw652Z6S_rUcZbv3l>n((KADB?_zV?uxafhC4S%fwcV$hIZlkqY* zQ`e1Xck<6v=+RHks>TU1>^e$b2{IMs`t068y%mrm^o`b`D&^?6fkh@6)Cx)yjRNf% zR7St1t{G#Nu5pBHp})5@#S>M#qY-qNX0S5jg=D;mm;Sni!%EE6ok!73qVQpaV$z!` zYgaJla$2#xG-HSlXTFbcTA&zSV_1(1i2+W7%qP0jZFzli@NHf4;oLpXDMX7|L0n$s zy`xvbkQ;?>dq^L9{I9+AghpI9tak?yuh+uk&s2I{_|B-(_mR}n7NJifF*tzF?iJjtZZDEHT33pJ<_raK+@D!iAby;?QCcnP(2c!~V_p%zX6u^9A8TM8^t>uf z_c_ZBjhpkyB2&l~Gt&U2xQAAif^ZJweI(yWlWiUg-h1>~s5$aelH-U82h}dtY#!e| z$jjF(PI~?)BC+V^;^x#2L27fVET=ys$oYvUw0qjX*?d5`Yh{Rg)=Qt|?gHP9IZEE^ zm$#m9_mf3EtGq580)L}n!>)H03}%BB(T7vN5n84XTeH#{xI^5NK6W%ip!WV#NCwV%{BI&a=b}rCK;23)FZxdMdOx z5h=wSNwHbZA0y<+_K$s*lWxAWFJ;V-K}^+oqOX<7(*m+s(F(tbAoZAD%b-KwuPD{L z;35C^W-r5l(cejfZ0((2%r!s1#@vL6p_$G)7# zQ(4y5RXTRY_j`xsN0-?4K3>z4C3jQ9X@ae9WOiL71r=)`^`cYh&-mvgfia07SZOhM z3!fo~o1wF|c)Z~K%u!TdtzMoA>Y^8s6}wdC9+VG{yDh1CQp;6QI0tnP360GEd|Q2V zB)Toz`9_syCI>O&yGz;DIp~4&X)jRUmAQH^d2f0$yn{-oFCC}X(mMP$;?FWx@Z|pT z%%>w-5waw;z_`DAaW#1~6q+$1n~VFre~z5i+sNELriO7f1y5t?+u)~n8ms&6gpdS? zX6KhWs8Cy8M_lO^CJ|T6f2-&YF4KeCIc_wREvR8w zW$JofUbv@1uiX2`(+!CC85e;G$)|etmXSp$xyP|KZ(ZH)wu49@JEwRHpm4RDV1Mq0 zRcV?sG+?o!0@UWCcewoPX(7XEe+HE-mTACLz^^cKadA*s zu(W1J=APAw#I67%46D*t*=L2YA0wPv^+@;$l^d}=SUfwF>Oeeq z|E`;doN!2al_+@7&PZyn1tZIS3ex91ncr2A;JsI%iL#|A8H*scHLwkMBQ+%~_Sl;c z<1NAi)Vq9@921#p`eVzN{<=>}k0MQi3q!D4mRdl)CQa6SC>m5gpOF!>LWxuIZSxuB zCG@x%IS0;BmZ){(lGal#0hH(cu<65@h$`;qZz&5rXCXmeYQ~YPgCdm!ID(n-4h@46 z0EVF+qv&Im`txPY&gw}r+95Kru}%2SM^2}G>*m(S)RAPHZ2S8o@mbRcU zN@K+{7wQJio9ymtj7q=NVZ^;aefw{Hg-CP(?feIohU}!5q zq=UvXmJ63~=DM(}%+-J}kVVfh`}GsfaTL0%nOY3Afn5P9+MQSXs;iVT`Kw6g_ITd4 zt(Hb%JXM<3s8fgzukb2SUj+H*sga|D)3=~=Engk<;$H*v3UPRrdyxqeFN`YhKwQkb z@=Jy=3&lSO?Hm?@idbQ zsgoekQrxQO5TC6J@2(Rq3X_#V*n~-af524qMoM9NvPTVPu*_uds~>6mpV&D07dVU@ zeP^S>4u(|)D`XN~d8-_8;X$pC81{(3?^kttTnI+oJk>B=Cmy7el0JO_KM?FUgmjKiEkOaGE9ehzB zBNL|A%?o1$#R{1u^!nIS_Xq(3_NlJVt#f$1bREZ^{{4{q!yif%+e-6$FWE*5g&JYT zXcO;&(}j`=FR*aTj9Tr!k>basi@^8b0t5Jr+b!Z26>ph`Jn+g+i?gtLDMIS6JJ_A! zS%*g5<4prUuUEj+H!SgI(wd#y2wdKJ9P*y};_hX-ucZ{;ss&cEt1||@B=MX9|L0@7 zBdH&P!pOkle{~ma>8nnH)iAI|bh;7o;LRa71s4Mc{Lg-n++MiH0up)OX0AQk(EqG? zo+xev7%Glj%h{~f87_CZ?!6F^uB&-5h-gziKzC%Ee$$;uI8D3F*QKwVStnWnK@d7S z&s_JeD6Rax&=Ou?ekKWl+U>m~u${8`30U~*Y^(E-7N2=<27~QHq>sfZkW^j0znu9W zp#jw#1=3f=K#r&_?VKBSspBF!58NykX76_U;xO25Kn^#sF6;gg_h_hS%Sf^Ghf&h~ z==`InpVXftkL`v07kBcc*j4y0DFN{J+$b-nJH%HWww93xtfX+1BIJBux&4OCU-uT5 zvJNu{z8KF_9+xZCA++7e-UDg_HE&g#v99!+uj2wiEV;&#Nasi)|E4O%SPJm9C$Y7s zqh&Zk;3W+C@Lxf)B~>(sf5(m&G0Hy{tOu`TiHjvY3jU>edik%b&a9;QIBs5TiMRT0 za$CHQ7o|7t&9m|}tC>LxC_QXXT0WRknXcpM|Cu3$us{^^JB!IizuX|kf%n$%C34AM zoOO+-XrHvtvg#y&IDk~^Naz0p*|naj%34ekge5kBMsUGN65@1Km9Hwz{dq3_7Chj- zp)gP#uoa5OTbarOTQQrortga^2jg?v{a&U0E+d>D;t^a8MW^{13oaiF)}^KVf1Yt( zs(t)NWV{V>-Tj7TEnl9V_3OY&Hmk4ve$J65ZNuohO z8nauY>VGJd#xDwWY7BVb<_xEasiq)Y+=v9+BcEa03*nLvwi54sl8Dw}EO8Ipf50Oy zV#`}FaIafU!sWLCK-y$a9C>v);ImzGX5xKU+ovQI4$gCW^A;GKP?#};x4ALNOE(({ zE-8{eMJ5^mjQoLZKNenojsmNtqa4Oti|^c32z#>z0#HPbt{lc(^ZZ|NPm5xWsWo0T zPH^6>px9u>CQ68b+girlYDFcmvYFLMLS3J`pDyviQDMZ0$$G=FM2dn`w@fofe+lRVbBhB(M`sJazuV4;A|A zZ-YtawlmhzxLFxFFG9BMIu}5QX-5C zl%Ki*s|&_jTkUunV?j{*6E-7f8g>Uu9Rf34E;Z^!TWQ8qg*iMig!lFR+Hb9|rwdDe z^aFa|-7om)PFq=^VJ2tvaci*qW|S`vM0N^R=uGaFP*NvbjwgS!!3O<uwo zhSWjsMr&wYjmV9S*8*v>PTcB15a6RPZLUKu2z|q6jEYDsboymJ+B_DbJR4`V#7&vbnqYeA zHx9i;!l6~uE&rgXz*}TaH|CA=Ygey)t|ap0>Aur29cAziBZUa`0eKOYs%{e!q>C}d z0l^f=&ip~hRGuw!5>6$Sv7>zNK9l|L0FX5s4~nS9GK(9?dO-)cU}obQ-8r}ESXLm{ zY(vuqrsFa;*jMkWq$9?U z;hPN`Yxv;j=;N4?-`dEpU7P}TaksFm{-3)_K_J;iz^|G&rveFE9d+f-;q0u(r8CRu zn{RMk=1i;#(;uEz^Dyc6-S$0#s4O6SN!&uJMH-gqpbDuhLBGqR?HiqXWAqm zbl>{`%7LXuqVFQL_7Mgx&|;`R{okVvN?9UL@dCkV(X3R09B%!1BS!NzJ?brc zlxAx`84Cjb%})CAHf{i!5-Ae@7AhRFrLJ*8g34IY`l&sG(vkdl4Q&PWV8U)GBtke; zXJ9KmY7c+}HY0m++*5#23|_k7>=GJiu)HobMAbi4^aoOs8NCo1d;AKE&t@hY$oF-tt5ri9*XkrYJ(;a9>%7)9~hG`QOP*=HJ z-JY66dA zaua^@(WNuKV*=CBQEGwsyDM0IUwx4+=c;l%V#<-t3S2$j`fodX$O$!@ML%^`!W@SyLO;m9x*# zQ}Ss#Oih2VM2oa1&&*L8Wsa04CnwYgGyt8tEJRY{tG9&M62?#>XmIJ1>H^w{l!$MD zL_BZA`3gv6R&|@T0i-Rw;loMOH~L)Q*R1-2S)IX}FiZz8{uyG5aVl(YV7YcNuZ;&d9+7PT|6=*Qlv`RS*CD(xLZzleLfNBMS!dk4-|>*?B086MohX)gXNOwM zE7y7?dk#78_t-vm={?MtBh&e&xE>=;Fcdd`N^(FFFCG3zKYQZX^g=DT+&s-{Y1uh? zTi+hNbxUZzOr`7fg9W}p5Ba*?JD=C19G(b#tg#*dXHY1AKhbG zHT)>e^GKKFSl!Sq*mu34&!rY9eXCEc)Q9!7{O30Yf4(+yR4L=8u^6!Ku7(U5tDlin zxL#pIZk3j?rA+?t!b?A=$Y-bq{@AD7C2hySWInz7iZd&?1%Dw<4e;bix%BDjbYMlN z*9B#Fv+%UOsC|qdVI8g0Xlcz$x`Azq%8VHN(iiWY|JTj*IkJXDR9tnUE$JaeCI}$F zE_1`%$lyRIEK~(3Thd8F=Llsdj~KRLGFd82RxY*U!Q8ri$pCQ%(JiKYU>@8Jr0M-j zsJ3jsL^q6z{F2}S_JknsR8JY=8+_|Sd>z%pvBw+Thz3sqAR6K8?v%W%ZKO7@jL+jJ z^q!YRF8vt+IK@@kNwtZwy@HO6;nqylHM%|2!PB_M} zGOb=Hb?Cai-J|A>@;=E?9>~~aUvyKW<*vo8uIMK34OzrJ%~I$|gNWrGciDmZQf>Gu zBNwoOclKtp^mnT{BGwoF1`alI0EWkjg_@^j6huD^U%BdGj>WR!<;Z(; zp~0u!Zs)a0dN3_jV?v}9Tq)0jYR$Vm4o_4kr=|x@*)^`RHvk~Rb80{FmkVBk#nP$; z4hXXJ3L5_k_6;+_&PMPEk9|?_kKZJ9(~YmF!GA0%m&f`2)hmw7N;(U1O0bO2`s%H( zveW}P+SU%C8J^0u9A!9dlsR_6{)lfm>BXKAF0)4U4*8vw0$dhcDVjX1lY}e3NV1}nr1BRz-ASiX-4-&vG4)Xis|TVf9eIEc{E(At?yKUb zeF(}Lo_4K15HJ`*ht8s{&`PT+-dXjG1Kw%b0v56Z?xYp5`|;O`ScS>)&fS5g*Y9^r zDd7IHe~iM5TA#gp2_V26Q%lW`T=+}5Y*E)Nj*ae@U6=BBVh5%(Z`7R!O0x^<%nKRG z_**T9^rv#&E{bvKGD_zB%Ue)cZCem;|HDofYlYH8;wrm)@9eBo-IZ?! z6EDHR-!seTCPDlF>IN8Ze*N?^*Xk{$0K_5>WO1NL9hqD$YQTp4bWlpn7l#YsQTd-Hvbo3^A`|L{77_0;RTu^D=kfL`817n+;rJFtnAdC zziKV4r0X=5(0-#4evn9>h_p=urnSuPZf}b#b;McqW`gz6Pvvxz{B^7#ttxcNkjwBB zj_8ZQF+9+e`_T<1g0AS6u>e*(3f?F7t2fMCbO#wqrejx4yh-|1PCxlNI$^+{4uekl zh14Hu%=Pj!%NKo5CgcqWTztDP%t+?LShyAv>Yv)$W{-zIoF*b@&4DD_h zTj*&cT|x632+)gF(5&Mu@w`W<%%@taxqO|?B-0qZv8h_ZhaEP}uVSfJZj@4JMRtGU3X%n8`~~cM z>%yIv2{K8DI>EkZZ#=CT^`C7}dQLNzHW#OU2bC9z>NgpEpNd5WMr9Y)rj7Op#Mpjy zup|#*OVAovRpD(=%qL+O8X6wO?cT!(2c~(RU&$Dp%++4Pw%lD>30SLSiEd+e$ejn0D2h#|gsqvmCDn z)aTA}7QQ!{fN7y{BK3N^d?ncwU|9;Q;<3zAg!~{y_wan;8s5O zM}LjZCbY#wAIZ`!Ns8Tj0v@bkZvdz-MwZbyY@X-TT!H*Q4?iu#h(L|N+OkAq%E#Y3?(YsB8z zzyFx}c|}(>bn)3QHCv2!>j8XglZQuO=*U5uvnyPY^l)FzTR2_a{EL^+mreuIneF=|dbnSbp1+wWQ00Mx< zeMcfoPi2d%b!X36+Kgh zX-0`XLsCu}z*4NE-fDc5Ix$shKxos7l~yCbx@VJ7rg{ar7Fl00f=pF~JesdsG2HHs z1Ul2ClDvgvDn2k^frau9@*B3~jn3@nDfrI)w6|c*M{ARg~)BEZEBrA}hOW2!lz!5PLU$~r7o;oarV)h@DDwz>;n)yRb z&YUb{6#}rDtf3lqLgY*I)^3?l(TP5gsgsq=s{Q^w7pNa8Rlk_LbwbVbXEeMdqV+dW z5D1dZw5VJGvHX4i3x^;%_adCHF0XKr{aGWV#p;1wQBRK4%|8K>vqs#EoJTj>QD{1J zyQ3=cGoq9TA_n8K1kC9zjIS!kVFgR4(C&VBpiPGBb5^I6vk_AD)2`fQa5L5QZn3kP zYv#@FKuhB=(Uj0k+wMbm7j<77V}cCrbXQ%6>^7`*54>M4&iG|jse+rzytUB>Y{8R+ z3rTq2KX36wq=|=n&M4dYMn%_zhOw(fN>U(UKh5=ts?0VKY*@SfSV9Dch7| zb3RtH>Zm1IKX%df;U2UXgL<>@Dr=6FSEi)RGq_Mn`p|$?R-bKky{n31W|3#En86)+ zDo2d;QhCOe zrEY!Q(4z{s$!vS4Gq~66N3q08P=p%^0Q@IFFf|R zOV{_#-v|PR2FAj6fQZXDC@9bJY9bMe|4ush*x5I4TTo*s}i_vwh+B z@=y@jIzv+eY3kw`pYCPA6fE(lA-z1N`aj2k+Z2{G`oP^-U1n zs;UAle7QbSeg8dyXIRF!Lj@`-6}hA_NPp>-KH-J;^iK;^$ePJ@bv`|FhPW{FD>8QX{eI zzE&T2{2(aGR#a5f*4EZ)YiWV`o)?vtmKFnl3>b~yuWluI>(;H@=UVoUk9$&Uo12^a z`T2p<4B*(viHSZ%&{6kK6!i|iPO#CUQ z+CPFQMnUv9UI`JSG?8F+qs0og{)yCS55qKp2cObdP86IG9+Hh4F;gSj2c-!Ro+yOr zMSkO5KOPdq_T(C6@-f@X5XtZRIp|FeBlzxy+3whv*0xscD*wsxok?j(tXP?ajfSs3 zVhP_Kl3Xptm9=+QSB@)u!|is7^Y&K0!b;fI{bvUf4|NO8>xpc~5^4}y?!i5CW2rRd z9}Wci%_~O-u+)VI8Q=Q#5yL~!7yk+dq@<)26cj8kFONwlXB zG9n8D94d!vv-x<;(!#^TOG-*wT3T{*1&L2pSz?lOl1fGH6JzgUUraatSSbh(yRym7 zxxH-q;=~O`^n2fsV`FDd2ihjMtO)09AG~9u-yiIJ=%UjY^6_Pq@k z5hWZ0L_!861VNB4XDAhv?go|a5{Vf=Nd=Kqx}>{ABnCvJQ@R^QhGrOs<~ykO{{G)A z*K#o~7w0|ij^}yyK4f>fqo2t}d~$$;VWj!rUj#l{ z`I;Le7yd4XV03bFa_a1}4tw$pi9}jkTYGxO_q=EJN^{Zf{eB`BL*{zVuIrvLT=Jnz`etZ3V* zLx^1Q5_?E`7{1n71bnbZx0MtSy*6kAh`8=i~KK>we^>ymzKuX zUT!y8gJ;&vzXvlk<$8!GIU!&wD{Wa!0KV@;6h5;sI9R8 zS4ffiC`sA(8sBXviM<=*k0znpxJzW5Qxm^}@Jy@UrtpKb46BB$7Ckk-a#BLTY=wk8 zhl^Q_HTDbx&fXGO$9FmVs~BQ8FtD7TpC8O0#k~&}h$(R+4sC_WG+~?B1+W@kFwUpj zRhaU~XQCe8p1=KAT20bR5c&R6?^=7j*Vv)zec$_?b;CAY({vqMrN6vBS4z|zIBRvN zrn%FVmRDLAxFN`;Lx`h=4gz?d?x$CK;g6l!OFvWR83oxE-#vDSvP-3FYzpP)j$?q@1aAG z4vu4c>f z-S~mF*^El@{5=j}M})Ta_C>|Tw|UN4SLlJ=0K;}%A;Ew5DKH#ZI)s!#rWq%Uf_5OruGt%Tw0y3uOGAq zPFYw`z{SIZHyU{IhB?cCUCo(gZ-U4aT*@8g#&p~d-Qe;mNEzCthW(=i%M7*p)efjbcISu9w#U>@0k-Yi4m3gK%Ha1>f zFc0k&@U}rt`tjqzq~9$+rLDham#%Jyq+2p18Jj+Pl~D%;rh7HM(bZn686+E|?G0P| zeq$EY?WnGkI_QP%lk0E$>ygE~ad+df^a|`#j`!LMYY-vQTS5w5nL1mgh3jt9$|+M2 zmTrA%108*R`66*TIy!J~qp_aebLhWox?>Ikf|ZpO2;8=|Tps3)V`F+?OE z!mTpTa7HUD#Z6#o-4`nu4yIB~pC#Qb9+2T`cDdqT zKajR&d_O-pILI=Q05&frWxCSA7%Yb<_}EbDVwUH5-c~vag;5j9z__Q|ngy@)goz8h zRGnC{LjGIFjA3hyM1V>iFRUkB_~jr-Q~no0W#y^xR)GBUDNqAP!_ zO>XUh>gSS@lCNJ!tic3?U2yHW2jX&R`Wlh0GfU2L)HqC1l%Z@xe(8E7PBe!DF%VRo zyg^EebO?{BM1RYAC7AzugVYy}A2OeRHi~#u?g5VgHskr@ByCbY{zyt<8nRG2v+c$1 zrQ0-@KW)wBNo`TU*=h*NYXaH6qMkSt~4u-)a=2I6S{W z+84D8!6u8c=Z-YKz-JldG=cWw9j-pK(YchXnWLN2i12e3)GcwJR;CxSJpDlK?w}HJWY9i4yZRUcVvWoi z>v>CQ%pm0M*H__Ev0j?dWp=VO&-|8vW2;P2Jz&w&)&`N1KtWlVhl^_fM*r`=C>0bG zusBV3c6J^eAtxJ|{Ct5W(NH;O_;^{+2C-QBgr z&nupmxSiY4X2+Aq9ei@$5a=78^fEsX_mr9^2vcGm#vV(Sh2f?&(_0-wAF)gmQC@Vd z<9zdLWPJS0a{5T(Q&KGcb28c=-@i-u5srA3WWGh@&q3WjK(T*SAU8V8 zzWwV}XN=sbz&F{*pvoRi!6<>nk5!_#DK{dD4@|#^axxYx)OiBi&ggX)l!s{Tod4db z{>qJbnHLACsFB$<#lh6b-|sg@9ac|mT(R;^y$sTt3w`QDeqI_v6$ z<<^V8BSn@|dUm4bKhI?IP4}S7_&jr2Q${J;%*$2WjjmOF(7;| zM4ln0%uLq*tSKYyvtD6lW+wQ0p#d^JKJFU!hWuPJzIbN^B~@+E5$#1&&Zo1Y;@wzW%3%;;{E^vy z9n`9aql{qO7mFF}t6bCV*(8BD`30NdyctOr4W�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%0A=LrydN)47Oq-@>Nf_-r{dHrF3>!HrLt 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 literal 0 HcmV?d00001 diff --git a/docs/guide/register-simplex-name.md b/docs/guide/register-simplex-name.md new file mode 100644 index 0000000000..df78b19844 --- /dev/null +++ b/docs/guide/register-simplex-name.md @@ -0,0 +1,123 @@ +--- +title: Registering a SimpleX name +--- + +# Registering a SimpleX name + +A SimpleX name lets people reach you by typing a short `@name` instead of pasting +a long link. You register the name on a website and add your SimpleX link to it, +then claim the name in the app, to connect it with your SimpleX address or +channel name. + +> Names currently run on a testing network. The registration page is +> [testing-names.simplex.chat](https://testing-names.simplex.chat) and names end +> in `.testing`. The steps stay the same when the main names launch. + +## What you need + +- A crypto wallet such as MetaMask. +- The SimpleX app. +- A link for the name to open: your **contact address** (for one-to-one chats), + your **channel link** (to join a channel), or both. + +## How it works + +Setting up a name takes two parts. On the website you register the name and add +your link, so people can find your link by name. In the app you set the same name +on your profile - to prevent any other names from opening your profile or channel. + +![Steps to register a SimpleX name](./diagrams/simplex-name-steps.svg) + +## Step 1. Create a wallet + +Install MetaMask (browser extension or mobile app) and create a wallet. Write +down your recovery phrase somewhere safe and offline, as anyone with that phrase +controls your names. + +On the testing network you need a small amount of test ETH to pay network fees. + +## Step 2. Register the name and add your link + +Open the registration page and tap **Connect** to link your wallet. Type the +name you want in **Search for a name** and open it. Choose how many years to +register for, then tap **Next**. Names are currently 6 characters or more, and +some names are reserved. + +![Register your name and choose the registration period](./images/register-name.png) + +On **Create your profile**, paste your SimpleX link into the matching field: + +![Create your profile](./images/create-your-profile.png) + +- **SimpleX contact** for one-to-one chats. Copy your address from the app + (**Create SimpleX address**) and paste it here. People reach it with `@name`. +- **SimpleX channel** for a channel. Copy your **Channel link** and paste it here. + People join it with `#name`. +- **Advanced usage:** fill both to use one name for your chat (`@name`) and your + channel (`#name`). + +Then tap **Next**. + +Finally, tap **Begin** and complete the three steps the site shows: + +> Before we start. Registering your name takes three steps: +> 1. Complete a transaction to begin the timer. +> 2. Wait 60 seconds for the timer to complete. +> 3. Complete a second transaction to secure your name. + +These two transactions protect the name you are registering. The first records +only a secret code for the name, not the name itself, so nobody watching the +blockchain can see which name you want. The wait lets that first transaction +settle, and the second transaction then reveals and claims the name. Without this +two-step process, someone could see your registration in progress and grab the +name before you. + +![The three steps to secure your name](./images/before-we-start.png) + +## Step 3. Check what the app sees + +Copy your `#name.testing` (or `@name.testing`). Paste the name into the search bar, or simply type it. + +The app reports that the name is registered but not yet added to your profile: + +> Unconfirmed name. The SimpleX name is registered, but not added to profile. +> Please add it to your address or channel profile, if you are the owner. + +This is expected, and confirms the name points at your link. The next step +finishes the setup. + +## Step 4. Claim the name in the app + +Set the name on the same profile the record points at. + +- For your **contact address**, open your SimpleX address, tap **Your SimpleX + name**, enter the name, and save. +- For your **channel**, open the channel information, tap **SimpleX name** (below + **Channel link**), enter the name, and save. + +This proves to people who connect to you that `#name.testing` (or `@name.testing`) is your name. + +## Step 5. Connect by name + +On another device, copy `#name.testing` (or `@name.testing`). Paste the name into the search bar, or simply type it. This time the app connects, and the name +shows a check mark next to it. + +You can paste the name in your own device too - the app will warn you that it is your own name. + +Anyone can now reach you by name, and their app confirms it is really you. + +## If something doesn't work + +| Message | Meaning | Fix | +|---|---|---| +| Unconfirmed name | The name points at a link, but the link's profile does not claim the name yet. | Do step 4: set the name on the profile it points at. | +| Name not found | No name is registered. | Check the spelling, or register it (step 2). | +| No valid link | The name has no contact or channel link. | Add a link to the name on the website, on its **Records** tab. | +| Error saving name | You tried to claim a name that has no matching link on the website. | Add your contact address or channel link to the name, then set the name again. | +| None of your servers are set to resolve SimpleX names | No server in the app is set to resolve names. | In server settings, turn on **To resolve names** for a server. | + +## See also + +- [Making connections](./making-connections.md) +- [Chat profiles](./chat-profiles.md) +- [Channel webpage](./channel-webpage.md) From 6150b35a2f10260a6feb552ccf84f02190918492 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 6 Jul 2026 08:50:03 +0100 Subject: [PATCH 20/21] core, ui: resolve untyped domains (#7198) * core: resolve untyped domains * CPError * only add domain when it has link of correct type * resolve first * handle errors * remove CPError * update UI types * remove unused name Co-authored-by: Evgeny * refactor connection plan * kotlin: show domain and alternative chat, haskell tests for dual domains * view/tests * update kotlin * dual domains accounting for business chats * refactor, fix * fix kotlin * remove comment * search * add resolve mode * local resolution * refactor, bot types * search both contact and channel by name * fix * fix searching business chats by name * fix ui * ios * fix ios * fix icon --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- apps/ios/Shared/Model/AppAPITypes.swift | 29 ++- apps/ios/Shared/Model/SimpleXAPI.swift | 10 +- .../Shared/Views/ChatList/ChatListView.swift | 158 +++++++++++--- .../ios/Shared/Views/Helpers/ShareSheet.swift | 55 ++++- .../Shared/Views/NewChat/NewChatView.swift | 61 +++++- apps/ios/SimpleXChat/APITypes.swift | 1 + apps/ios/SimpleXChat/ChatTypes.swift | 9 + .../chat/simplex/common/model/ChatModel.kt | 8 +- .../chat/simplex/common/model/SimpleXAPI.kt | 38 +++- .../common/views/chatlist/ChatListView.kt | 192 ++++++++++++++---- .../common/views/chatlist/ShareListView.kt | 2 +- .../common/views/helpers/AlertManager.kt | 22 ++ .../common/views/helpers/SearchTextField.kt | 4 +- .../common/views/newchat/ConnectPlan.kt | 104 ++++++++-- .../common/views/newchat/NewChatSheet.kt | 86 ++++---- .../commonMain/resources/MR/base/strings.xml | 2 + .../src/Directory/Service.hs | 20 +- bots/api/COMMANDS.md | 4 +- bots/api/TYPES.md | 16 +- bots/src/API/Docs/Types.hs | 2 + .../types/typescript/src/commands.ts | 2 +- .../types/typescript/src/responses.ts | 2 + .../types/typescript/src/types.ts | 14 +- .../src/simplex_chat/types/_commands.py | 2 +- .../src/simplex_chat/types/_responses.py | 2 + .../src/simplex_chat/types/_types.py | 10 +- src/Simplex/Chat/Controller.hs | 27 ++- src/Simplex/Chat/Library/Commands.hs | 174 ++++++++++------ src/Simplex/Chat/Library/Internal.hs | 12 +- src/Simplex/Chat/Store/Groups.hs | 13 +- src/Simplex/Chat/Types.hs | 5 +- src/Simplex/Chat/View.hs | 13 +- tests/ChatTests/Names.hs | 82 ++++++++ tests/NameResolver.hs | 6 + 34 files changed, 934 insertions(+), 253 deletions(-) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 9e27c6fa16..1a66156a49 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -131,7 +131,7 @@ enum ChatCommand: ChatCmdProtocol { case apiAddContact(userId: Int64, incognito: Bool) case apiSetConnectionIncognito(connId: Int64, incognito: Bool) case apiChangeConnectionUser(connId: Int64, userId: Int64) - case apiConnectPlan(userId: Int64, connLink: String, linkOwnerSig: LinkOwnerSig?) + case apiConnectPlan(userId: Int64, connLink: String, resolveMode: PlanResolveMode, linkOwnerSig: LinkOwnerSig?) case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain?) case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain?) case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) @@ -347,9 +347,10 @@ enum ChatCommand: ChatCmdProtocol { case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" - case let .apiConnectPlan(userId, connLink, linkOwnerSig): + case let .apiConnectPlan(userId, connLink, resolveMode, linkOwnerSig): + let resolveStr = resolveMode == .unknown ? "" : " resolve=\(resolveMode.rawValue)" let sigStr = if let linkOwnerSig { " sig=\(encodeJSON(linkOwnerSig))" } else { "" } - return "/_connect plan \(userId) \(connLink)\(sigStr)" + return "/_connect plan \(userId) \(connLink)\(resolveStr)\(sigStr)" case let .apiPrepareContact(userId, connLink, contactShortLinkData, verifiedDomain): return "/_prepare contact \(userId) \(connLink.cmdString)\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(contactShortLinkData))" case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData, verifiedDomain): return "/_prepare group \(userId) \(connLink.cmdString) direct=\(onOff(directLink))\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(groupShortLinkData))" case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)" @@ -814,7 +815,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection) case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) - case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan) + case connectionPlan(user: UserRef, connLink: CreatedConnLink, planSimplexName: SimplexNameInfo?, otherSimplexName: SimplexNameInfo?, connectionPlan: ConnectionPlan) case newPreparedChat(user: UserRef, chat: ChatData) case contactUserChanged(user: UserRef, fromContact: Contact, newUser: UserRef, toContact: Contact) case groupUserChanged(user: UserRef, fromGroup: GroupInfo, newUser: UserRef, toGroup: GroupInfo) @@ -938,7 +939,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case let .invitation(u, connLinkInvitation, connection): return withUser(u, "connLinkInvitation: \(connLinkInvitation)\nconnection: \(connection)") case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\nnewUserId: \(String(describing: newUser.userId))") - case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))") + case let .connectionPlan(u, connLink, _, _, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))") case let .newPreparedChat(u, chat): return withUser(u, String(describing: chat)) case let .contactUserChanged(u, fromContact, newUser, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\nnewUserId: \(String(describing: newUser.userId))\ntoContact: \(String(describing: toContact))") case let .groupUserChanged(u, fromGroup, newUser, toGroup): return withUser(u, "fromGroup: \(String(describing: fromGroup))\nnewUserId: \(String(describing: newUser.userId))\ntoGroup: \(String(describing: toGroup))") @@ -1386,6 +1387,20 @@ enum OwnerVerification: Decodable, Hashable { case failed(reason: String) } +struct ConnectionPlanResult { + var connLink: CreatedConnLink + var planSimplexName: SimplexNameInfo? + var otherSimplexName: SimplexNameInfo? + var connectionPlan: ConnectionPlan +} + +// APIConnectPlan resolution scope; .never is local-store-only (no network), used for per-keystroke name search +enum PlanResolveMode: String { + case allGroups + case unknown + case never +} + enum ConnectionPlan: Decodable, Hashable { case invitationLink(invitationLinkPlan: InvitationLinkPlan) case contactAddress(contactAddressPlan: ContactAddressPlan) @@ -1401,7 +1416,7 @@ enum InvitationLinkPlan: Decodable, Hashable { } enum ContactAddressPlan: Decodable, Hashable { - case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?, verifiedDomain: SimplexDomain?) + case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?) case ownLink case connectingConfirmReconnect case connectingProhibit(contact: Contact) @@ -1416,7 +1431,7 @@ public struct GroupShortLinkInfo: Decodable, Hashable { } enum GroupLinkPlan: Decodable, Hashable { - case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?, verifiedDomain: SimplexDomain?) + case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?) case ownLink(groupInfo: GroupInfo) case connectingConfirmReconnect case connectingProhibit(groupInfo_: GroupInfo?) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index d7ee777d5b..5e957d68dc 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1026,13 +1026,17 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi if let r { throw r.unexpected } else { return nil } } -func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> (CreatedConnLink, ConnectionPlan)? { +func apiConnectPlan(connLink: String, resolveMode: PlanResolveMode = .unknown, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> ConnectionPlanResult? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectPlan: no current user") return nil } - let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, linkOwnerSig: linkOwnerSig), inProgress: inProgress) - if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return (connLink, connPlan) } + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, resolveMode: resolveMode, linkOwnerSig: linkOwnerSig), inProgress: inProgress) + if case let .result(.connectionPlan(_, connLink, planSimplexName, otherSimplexName, connPlan)) = r { + return ConnectionPlanResult(connLink: connLink, planSimplexName: planSimplexName, otherSimplexName: otherSimplexName, connectionPlan: connPlan) + } + // a .never (typing) search that matches nothing locally is not an error to surface + if case .error(.error(.notResolvedLocally)) = r { return nil } if let r { await apiConnectResponseAlert(r) } return nil } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 01887c12be..bcef5cecf3 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -151,7 +151,7 @@ struct ChatListView: View { @FocusState private var searchFocussed @State private var searchText = "" @State private var searchShowingSimplexLink = false - @State private var searchChatFilteredBySimplexLink: String? = nil + @State private var searchChatFilteredBySimplexLink: Set = [] @State private var scrollToSearchBar = false @State private var userPickerShown: Bool = false @State private var sheet: SomeSheet? = nil @@ -511,8 +511,8 @@ struct ChatListView: View { // Spec: spec/client/chat-list.md#filteredChats private func filteredChats() -> [Chat] { - if let linkChatId = searchChatFilteredBySimplexLink { - return chatModel.chats.filter { $0.id == linkChatId } + if !searchChatFilteredBySimplexLink.isEmpty { + return chatModel.chats.filter { searchChatFilteredBySimplexLink.contains($0.id) } } else { let s = searchString() return s == "" @@ -626,13 +626,21 @@ struct ChatListSearchBar: View { @FocusState.Binding var searchFocussed: Bool @Binding var searchText: String @Binding var searchShowingSimplexLink: Bool - @Binding var searchChatFilteredBySimplexLink: String? + @Binding var searchChatFilteredBySimplexLink: Set @Binding var parentSheet: SomeSheet? @State private var ignoreSearchTextChange = false + // when the search text is a SimpleX name, the string to connect to (with @/# preserved); nil otherwise + @State private var connectNameCandidate: String? = nil + @State private var nameSearchTask: Task? = nil var body: some View { VStack(spacing: 12) { - ScrollView([.horizontal], showsIndicators: false) { TagsView(parentSheet: $parentSheet, searchText: $searchText) } + // a typed name replaces the list tags with a row to connect to it (as on Android mobile) + if let candidate = connectNameCandidate { + connectByNameRow(candidate) + } else { + ScrollView([.horizontal], showsIndicators: false) { TagsView(parentSheet: $parentSheet, searchText: $searchText) } + } HStack(spacing: 12) { HStack(spacing: 4) { Image(systemName: "magnifyingglass") @@ -675,33 +683,50 @@ struct ChatListSearchBar: View { if ignoreSearchTextChange { ignoreSearchTextChange = false } else { - switch strConnectTarget(t.trimmingCharacters(in: .whitespaces)) { + let s = t.trimmingCharacters(in: .whitespaces) + switch strConnectTarget(s) { case let .link(text, _, linkText): + nameSearchTask?.cancel() + nameSearchTask = nil searchFocussed = false ignoreSearchTextChange = true searchText = linkText searchShowingSimplexLink = true - searchChatFilteredBySimplexLink = nil + searchChatFilteredBySimplexLink = [] + connectNameCandidate = nil connect(text) - case let .name(text, _): - searchFocussed = false - planAndConnect( - text, - theme: theme, - dismiss: false, - cleanup: { - searchText = "" - searchFocussed = false + default: + // not a link: a recognized SimpleX name shows the connect-by-name row (in place of the + // list tags) and, debounced, resolves locally per keystroke to narrow the list to the + // matching known chat(s); tapping the row connects online. Clear the filter immediately so + // the list falls back to text search until the search returns. + let candidate = nameSearchCandidate(s) + connectNameCandidate = candidate + searchShowingSimplexLink = false + searchChatFilteredBySimplexLink = [] + nameSearchTask?.cancel() + nameSearchTask = nil + if let candidate = candidate { + nameSearchTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: 300_000_000) + if Task.isCancelled { return } + // a bare name can be a contact or a channel: search both and keep every match + let targets = candidate.hasPrefix("@") || candidate.hasPrefix("#") ? [candidate] : ["@\(candidate)", "#\(candidate)"] + var ids: [String] = [] + for name in targets { + let plan = await apiConnectPlan(connLink: name, resolveMode: .never, inProgress: BoxedValue(false)) + if Task.isCancelled { return } + if let id = knownChatId(plan) { ids.append(id) } + } + searchChatFilteredBySimplexLink = Set(ids) + // drop the row only when every searched type is already known locally + if ids.count == targets.count { connectNameCandidate = nil } } - ) - case .none: - if t != "" { + } else if t != "" { searchFocussed = true } else { ConnectProgressManager.shared.cancelConnectProgress() } - searchShowingSimplexLink = false - searchChatFilteredBySimplexLink = nil } } } @@ -730,6 +755,33 @@ struct ChatListSearchBar: View { } } + // Row shown in place of the list tags when the search text is a SimpleX name. The @ icon marks a + // contact name, the tag icon a channel/other name; tapping hides the keyboard, connects online, and + // clears the field. + private func connectByNameRow(_ name: String) -> some View { + HStack(spacing: 4) { + Image(systemName: name.hasPrefix("@") ? "at" : "number") + .foregroundColor(theme.colors.primary) + Text(String.localizedStringWithFormat(NSLocalizedString("Connect to %@", comment: "new chat action"), name)) + .foregroundColor(theme.colors.primary) + Spacer() + } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onTapGesture { + searchFocussed = false + planAndConnect( + name, + theme: theme, + dismiss: false, + cleanup: { + searchText = "" + connectNameCandidate = nil + } + ) + } + } + private func connect(_ link: String) { planAndConnect( link, @@ -739,12 +791,72 @@ struct ChatListSearchBar: View { searchText = "" searchFocussed = false }, - filterKnownContact: { searchChatFilteredBySimplexLink = $0.id }, - filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id } + filterKnownContact: { searchChatFilteredBySimplexLink = [$0.id] }, + filterKnownGroup: { searchChatFilteredBySimplexLink = [$0.id] } ) } } +// Default top-level part used to complete a bare name typed in the search field (search field only; +// the message parser and the wire format are unchanged). +private let DEFAULT_NAME_TLD = "testing" +// Shortest name that offers the button, so it is discoverable but does not flash on a single letter. +private let MIN_NAME_LENGTH = 2 + +private func isNameLabel(_ s: String) -> Bool { + s.count >= 1 && s.count <= 63 && s.range(of: "^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$", options: .regularExpression) != nil +} + +// On-device candidate for connecting by SimpleX name: the string sent to the core to resolve it. +// The chat id a local (.never) search resolved to — a contact, business, or channel — or nil on a miss. +// A name-resolved chat may be prepared in the store but not yet listed, so add it so the filter can surface it. +@MainActor +func knownChatId(_ result: ConnectionPlanResult?) -> String? { + guard let plan = result?.connectionPlan else { return nil } + let m = ChatModel.shared + switch plan { + case let .contactAddress(contactAddressPlan): + if case let .known(contact) = contactAddressPlan { + if m.getContactChat(contact.contactId) == nil { + m.addChat(Chat(chatInfo: .direct(contact: contact), chatItems: [])) + } + return contact.id + } + return nil + case let .groupLink(groupLinkPlan): + switch groupLinkPlan { + case .known(let groupInfo), .ownLink(let groupInfo): + if m.getGroupChat(groupInfo.groupId) == nil { + m.addChat(Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil), chatItems: [])) + } + return groupInfo.id + default: + return nil + } + default: + return nil + } +} + +// Mirrors the domain grammar (nameLabelP/mkDomain in SimplexName.hs): an optional @/# prefix, then +// dot-separated ASCII labels; a dotless word is completed with the default top-level part. Returns +// the string to send (keeping @/# so the type is preserved), or nil when the text is not a name. +func nameSearchCandidate(_ str: String) -> String? { + let text = str.trimmingCharacters(in: .whitespaces) + let prefix: Character? = text.first.flatMap { $0 == "@" || $0 == "#" ? $0 : nil } + let core = prefix != nil ? String(text.dropFirst()) : text + if core.isEmpty { return nil } + let labels = core.split(separator: ".", omittingEmptySubsequences: false) + if labels.contains(where: { !isNameLabel(String($0)) }) { return nil } + if labels.count > 1 { + return text // already has a top-level part + } else if core.count >= MIN_NAME_LENGTH { + return "\(prefix.map(String.init) ?? "")\(core).\(DEFAULT_NAME_TLD)" + } else { + return nil + } +} + struct TagsView: View { @EnvironmentObject var chatTagsModel: ChatTagsModel @EnvironmentObject var chatModel: ChatModel diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 039c072c80..670cc7cae0 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -144,8 +144,10 @@ class OpenChatAlertViewController: UIViewController { private let information: String? private let cancelTitle: String private let confirmTitle: String? + private let secondTitle: String? private let onCancel: () -> Void private let onConfirm: (() -> Void)? + private let onSecond: (() -> Void)? init( profileName: String, @@ -156,8 +158,10 @@ class OpenChatAlertViewController: UIViewController { information: String? = nil, cancelTitle: String = "Cancel", confirmTitle: String? = "Open", + secondTitle: String? = nil, onCancel: @escaping () -> Void = {}, - onConfirm: (() -> Void)? = nil + onConfirm: (() -> Void)? = nil, + onSecond: (() -> Void)? = nil ) { self.profileName = profileName self.profileFullName = profileFullName @@ -167,8 +171,10 @@ class OpenChatAlertViewController: UIViewController { self.information = information self.cancelTitle = cancelTitle self.confirmTitle = confirmTitle + self.secondTitle = secondTitle self.onCancel = onCancel self.onConfirm = onConfirm + self.onSecond = onSecond super.init(nibName: nil, bundle: nil) modalPresentationStyle = .overFullScreen @@ -277,7 +283,38 @@ class OpenChatAlertViewController: UIViewController { let buttonStack: UIStackView var buttonDividerConstraints: [NSLayoutConstraint] = [] - if let confirmTitle { + if let confirmTitle, let secondTitle { + // Three buttons (a sibling action is present) — always vertical + let confirmButton = UIButton(type: .system) + confirmButton.setTitle(confirmTitle, for: .normal) + confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) + + let secondButton = UIButton(type: .system) + secondButton.setTitle(secondTitle, for: .normal) + secondButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + secondButton.addTarget(self, action: #selector(secondTapped), for: .touchUpInside) + + buttonStack = UIStackView(arrangedSubviews: [confirmButton, secondButton, cancelButton]) + buttonStack.axis = .vertical + buttonStack.distribution = .fillEqually + buttonStack.spacing = 0 + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * 3).isActive = true + + for button in [secondButton, cancelButton] { + let divider = UIView() + divider.backgroundColor = UIColor.separator + divider.translatesAutoresizingMaskIntoConstraints = false + buttonStack.addSubview(divider) + buttonDividerConstraints += [ + divider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + divider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + divider.bottomAnchor.constraint(equalTo: button.topAnchor), + divider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + } + } else if let confirmTitle { let confirmButton = UIButton(type: .system) confirmButton.setTitle(confirmTitle, for: .normal) confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) @@ -372,6 +409,12 @@ class OpenChatAlertViewController: UIViewController { self.onConfirm?() } } + + @objc private func secondTapped() { + dismiss(animated: true) { + self.onSecond?() + } + } } @@ -385,8 +428,10 @@ func showOpenChatAlert( information: String? = nil, cancelTitle: String = "Cancel", confirmTitle: String? = "Open", + secondTitle: String? = nil, onCancel: @escaping () -> Void = {}, - onConfirm: (() -> Void)? = nil + onConfirm: (() -> Void)? = nil, + onSecond: (() -> Void)? = nil ) { let themedView = profileImage.environmentObject(theme) let hostingController = UIHostingController(rootView: themedView) @@ -403,8 +448,10 @@ func showOpenChatAlert( information: information, cancelTitle: cancelTitle, confirmTitle: confirmTitle, + secondTitle: secondTitle, onCancel: onCancel, - onConfirm: onConfirm + onConfirm: onConfirm, + onSecond: onSecond ) topVC.present(alertVC, animated: true) } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 271c3845c3..a87b9b46f4 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -1116,6 +1116,8 @@ private func showPrepareContactAlert( contactShortLinkData: ContactShortLinkData, ownerVerification: OwnerVerification? = nil, verifiedDomain: SimplexDomain? = nil, + connectOtherButton: String? = nil, + connectOtherLink: String? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1138,6 +1140,7 @@ private func showPrepareContactAlert( information: ownerVerificationMessage(ownerVerification), cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), confirmTitle: NSLocalizedString("Open new chat", comment: "new chat action"), + secondTitle: connectOtherButton, onCancel: { cleanup?() }, onConfirm: { Task { @@ -1155,6 +1158,9 @@ private func showPrepareContactAlert( } } } + }, + onSecond: connectOtherLink.map { link in + { planAndConnect(link, theme: theme, dismiss: dismiss, cleanup: cleanup) } } ) } @@ -1165,6 +1171,8 @@ private func showPrepareGroupAlert( groupShortLinkData: GroupShortLinkData, ownerVerification: OwnerVerification? = nil, verifiedDomain: SimplexDomain? = nil, + connectOtherButton: String? = nil, + connectOtherLink: String? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1189,6 +1197,7 @@ private func showPrepareGroupAlert( confirmTitle: isChannel ? NSLocalizedString("Open new channel", comment: "new chat action") : NSLocalizedString("Open new group", comment: "new chat action"), + secondTitle: connectOtherButton, onCancel: { cleanup?() }, onConfirm: { Task { @@ -1210,6 +1219,9 @@ private func showPrepareGroupAlert( } } } + }, + onSecond: connectOtherLink.map { link in + { planAndConnect(link, theme: theme, dismiss: dismiss, cleanup: cleanup) } } ) } @@ -1217,7 +1229,9 @@ private func showPrepareGroupAlert( private func showOpenKnownContactAlert( _ contact: Contact, theme: AppTheme, - dismiss: Bool + dismiss: Bool, + connectOtherButton: String? = nil, + connectOtherLink: String? = nil ) { showOpenChatAlert( profileName: contact.profile.displayName, @@ -1235,8 +1249,12 @@ private func showOpenKnownContactAlert( contact.nextConnectPrepared ? NSLocalizedString("Open new chat", comment: "new chat action") : NSLocalizedString("Open chat", comment: "new chat action"), + secondTitle: connectOtherButton, onConfirm: { openKnownContact(contact, dismiss: dismiss, cleanup: nil) + }, + onSecond: connectOtherLink.map { link in + { planAndConnect(link, theme: theme, dismiss: dismiss) } } ) } @@ -1244,7 +1262,9 @@ private func showOpenKnownContactAlert( private func showOpenKnownGroupAlert( _ groupInfo: GroupInfo, theme: AppTheme, - dismiss: Bool + dismiss: Bool, + connectOtherButton: String? = nil, + connectOtherLink: String? = nil ) { let subscriberCount = groupInfo.groupSummary.publicMemberCount.map { "\($0) subscribers" } showOpenChatAlert( @@ -1274,8 +1294,12 @@ private func showOpenKnownGroupAlert( ? NSLocalizedString("Open new chat", comment: "new chat action") : NSLocalizedString("Open chat", comment: "new chat action") ), + secondTitle: connectOtherButton, onConfirm: { openKnownGroup(groupInfo, dismiss: dismiss, cleanup: nil) + }, + onSecond: connectOtherLink.map { link in + { planAndConnect(link, theme: theme, dismiss: dismiss) } } ) } @@ -1319,7 +1343,20 @@ func planAndConnect( ConnectProgressManager.shared.stopConnectProgress() } if !inProgress.boxedValue { return } - if let (connectionLink, connectionPlan) = result { + if let result { + let connectionLink = result.connLink + let connectionPlan = result.connectionPlan + let planSimplexName = result.planSimplexName + // the name can also resolve to the other kind; its type picks the verb, its short form the label and target + let connectOtherLink = result.otherSimplexName?.shortStr + let connectOtherButton: String? = result.otherSimplexName.map { info in + String.localizedStringWithFormat( + info.nameType == .publicGroup + ? NSLocalizedString("Join channel %@", comment: "new chat action") + : NSLocalizedString("Connect to %@", comment: "new chat action"), + info.shortStr + ) + } switch connectionPlan { case let .invitationLink(ilp): switch ilp { @@ -1386,7 +1423,7 @@ func planAndConnect( } case let .contactAddress(cap): switch cap { - case let .ok(contactSLinkData_, ownerVerification, verifiedDomain): + case let .ok(contactSLinkData_, ownerVerification): if let contactSLinkData = contactSLinkData_ { logger.debug("planAndConnect, .contactAddress, .ok, short link data present") await MainActor.run { @@ -1394,7 +1431,9 @@ func planAndConnect( connectionLink: connectionLink, contactShortLinkData: contactSLinkData, ownerVerification: ownerVerification, - verifiedDomain: verifiedDomain, + verifiedDomain: planSimplexName?.nameDomain, + connectOtherButton: connectOtherButton, + connectOtherLink: connectOtherLink, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1443,7 +1482,7 @@ func planAndConnect( if let f = filterKnownContact { f(contact) } else { - showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss) + showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink) } } case let .known(contact): @@ -1455,7 +1494,7 @@ func planAndConnect( if let f = filterKnownContact { f(contact) } else { - showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss) + showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink) } } case let .contactViaAddress(contact): @@ -1470,7 +1509,7 @@ func planAndConnect( } case let .groupLink(glp): switch glp { - case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification, verifiedDomain): + case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification): if let groupSLinkData = groupSLinkData_ { logger.debug("planAndConnect, .groupLink, .ok, short link data present") await MainActor.run { @@ -1479,7 +1518,9 @@ func planAndConnect( groupShortLinkInfo: groupShortLinkInfo_, groupShortLinkData: groupSLinkData, ownerVerification: ownerVerification, - verifiedDomain: verifiedDomain, + verifiedDomain: planSimplexName?.nameDomain, + connectOtherButton: connectOtherButton, + connectOtherLink: connectOtherLink, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1538,7 +1579,7 @@ func planAndConnect( if let f = filterKnownGroup { f(groupInfo) } else { - showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss) + showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink) } } case let .noRelays(groupSLinkData_): diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 47522a5deb..636ba99927 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -763,6 +763,7 @@ public enum ChatErrorType: Decodable, Hashable { case chatStoreChanged case invalidConnReq case simplexDomainNotReady(simplexDomain: SimplexDomain, simplexDomainError: SimplexDomainError) + case notResolvedLocally case unsupportedConnReq case invalidChatMessage(connection: Connection, message: String) case connReqMessageProhibited diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 3acc2c7eec..d85999a7e7 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -5290,6 +5290,15 @@ public struct SimplexNameInfo: Codable, Equatable, Hashable { public var nameType: SimplexNameType public var nameDomain: SimplexDomain + // mirrors backend shortNameInfoStr: "#name" for a simplex public group, else prefix + full domain + public var shortStr: String { + if nameType == .publicGroup && nameDomain.nameTLD == .simplex && nameDomain.subDomain.isEmpty { + return "#" + nameDomain.domain + } else { + return (nameType == .publicGroup ? "#" : "@") + nameDomain.fullDomainName + } + } + public init(nameType: SimplexNameType, nameDomain: SimplexDomain) { self.nameType = nameType self.nameDomain = nameDomain diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index ef5fe334b8..fd093f5761 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -4887,7 +4887,13 @@ enum class SimplexLinkType(val linkType: String) { data class SimplexNameInfo( val nameType: SimplexNameType, val nameDomain: SimplexDomain -) +) { + // mirrors backend shortNameInfoStr: "#name" for a simplex public group, else prefix + full domain + val shortStr: String get() = when { + nameType == SimplexNameType.publicGroup && nameDomain.nameTLD == SimplexTLD.simplex && nameDomain.subDomain.isEmpty() -> "#" + nameDomain.domain + else -> (if (nameType == SimplexNameType.publicGroup) "#" else "@") + nameDomain.fullDomainName + } +} @Serializable data class SimplexDomain( 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 bf0afe4546..80b7d828ba 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 @@ -1517,10 +1517,12 @@ object ChatController { return null } - suspend fun apiConnectPlan(rh: Long?, connLink: String, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState): Pair? { + suspend fun apiConnectPlan(rh: Long?, connLink: String, resolveMode: PlanResolveMode = PlanResolveMode.PRMUnknown, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState): ConnectionPlanResult? { val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } - val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink, linkOwnerSig), inProgress = inProgress) - if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan + val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink, resolveMode, linkOwnerSig), inProgress = inProgress) + if (r is API.Result && r.res is CR.CRConnectionPlan) return ConnectionPlanResult(r.res.connLink, r.res.planSimplexName, r.res.otherSimplexName, r.res.connectionPlan) + // a PRMNever (typing) search that matches nothing locally is not an error to surface + if (r is API.Error && r.err is ChatError.ChatErrorChat && r.err.errorType is ChatErrorType.NotResolvedLocally) return null if (inProgress.value && r != null) apiConnectResponseAlert(r) return null } @@ -3861,7 +3863,7 @@ sealed class CC { class APIAddContact(val userId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() - class APIConnectPlan(val userId: Long, val connLink: String, val linkOwnerSig: LinkOwnerSig? = null): CC() + class APIConnectPlan(val userId: Long, val connLink: String, val resolveMode: PlanResolveMode = PlanResolveMode.PRMUnknown, val linkOwnerSig: LinkOwnerSig? = null): CC() class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData, val verifiedDomain: SimplexDomain? = null): CC() class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData, val verifiedDomain: SimplexDomain? = null): CC() class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC() @@ -4070,8 +4072,9 @@ sealed class CC { is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" is APIConnectPlan -> { + val resolveStr = if (resolveMode != PlanResolveMode.PRMUnknown) " resolve=${resolveMode.cmdString}" else "" val sigStr = if (linkOwnerSig != null) " sig=${json.encodeToString(linkOwnerSig)}" else "" - "/_connect plan $userId $connLink$sigStr" + "/_connect plan $userId $connLink$resolveStr$sigStr" } is APIPrepareContact -> "/_prepare contact $userId ${connLink.cmdString}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(contactShortLinkData)}" is APIPrepareGroup -> "/_prepare group $userId ${connLink.cmdString} direct=${onOff(directLink)}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(groupShortLinkData)}" @@ -6513,7 +6516,7 @@ sealed class CR { @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connLinkInvitation: CreatedConnLink, val connection: PendingContactConnection): CR() @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() @Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR() - @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val connectionPlan: ConnectionPlan): CR() + @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val planSimplexName: SimplexNameInfo? = null, val otherSimplexName: SimplexNameInfo? = null, val connectionPlan: ConnectionPlan): CR() @Serializable @SerialName("newPreparedChat") class NewPreparedChat(val user: UserRef, val chat: Chat): CR() @Serializable @SerialName("contactUserChanged") class ContactUserChanged(val user: UserRef, val fromContact: Contact, val newUser: UserRef, val toContact: Contact): CR() @Serializable @SerialName("groupUserChanged") class GroupUserChanged(val user: UserRef, val fromGroup: GroupInfo, val newUser: UserRef, val toGroup: GroupInfo): CR() @@ -7103,6 +7106,23 @@ sealed class SimplexDomainError { @Serializable @SerialName("unknownDomain") object UnknownDomain : SimplexDomainError() } +data class ConnectionPlanResult( + val connLink: CreatedConnLink, + val planSimplexName: SimplexNameInfo?, + val otherSimplexName: SimplexNameInfo?, + val connectionPlan: ConnectionPlan, +) + +// APIConnectPlan resolution scope; PRMNever is local-store-only (no network), used for per-keystroke name search +enum class PlanResolveMode { + PRMAllGroups, PRMUnknown, PRMNever; + val cmdString: String get() = when (this) { + PRMAllGroups -> "allGroups" + PRMUnknown -> "unknown" + PRMNever -> "never" + } +} + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @@ -7121,7 +7141,7 @@ sealed class InvitationLinkPlan { @Serializable sealed class ContactAddressPlan { - @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null, val verifiedDomain: SimplexDomain? = null): ContactAddressPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): ContactAddressPlan() @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan() @@ -7131,7 +7151,7 @@ sealed class ContactAddressPlan { @Serializable sealed class GroupLinkPlan { - @Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = null, val verifiedDomain: SimplexDomain? = null): GroupLinkPlan() + @Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = null): GroupLinkPlan() @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() @@ -7441,6 +7461,7 @@ sealed class ChatErrorType { is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" is SimplexDomainNotReady -> "simplexDomainNotReady" + is NotResolvedLocally -> "notResolvedLocally" is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" is ConnReqMessageProhibited -> "connReqMessageProhibited" @@ -7524,6 +7545,7 @@ sealed class ChatErrorType { @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() @Serializable @SerialName("simplexDomainNotReady") class SimplexDomainNotReady(val simplexDomain: SimplexDomain, val simplexDomainError: SimplexDomainError): ChatErrorType() + @Serializable @SerialName("notResolvedLocally") object NotResolvedLocally: ChatErrorType() @Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("connReqMessageProhibited") object ConnReqMessageProhibited: ChatErrorType() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 99dec506e7..439d1af189 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -49,6 +49,7 @@ import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.seconds @@ -745,7 +746,7 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { } @Composable -private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { +private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState>, connectNameCandidate: MutableState) { Box { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { val focusRequester = remember { FocusRequester() } @@ -763,6 +764,8 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState searchText = searchText, enabled = !remember { searchShowingSimplexLink }.value, trailingContent = null, + // the clear button must line up with the filter icon it replaces, so no reduction here + reducedCloseButtonPadding = 0.dp, ) { searchText.value = searchText.value.copy(it) } @@ -791,30 +794,35 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState LaunchedEffect(Unit) { snapshotFlow { searchText.value.text } .distinctUntilChanged() - .collect { - when (val target = strConnectTarget(it.trim())) { - is ConnectTarget.Link -> { - hideKeyboard(view) - searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero) - searchShowingSimplexLink.value = true - searchChatFilteredBySimplexLink.value = null - connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } - } - is ConnectTarget.Name -> { - // A name lookup means "take me to this contact": open the chat if - // it's already known (visible prompt), unlike a pasted link which - // filters the list. So no filterKnownContact here. - hideKeyboard(view) - withBGApi { - planAndConnect( - chatModel.remoteHostId(), - target.text, - close = null, - cleanup = { searchText.value = TextFieldValue() }, - ) + .collectLatest { + val target = strConnectTarget(it.trim()) + if (target is ConnectTarget.Link) { + hideKeyboard(view) + searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero) + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = emptySet() + connectNameCandidate.value = null + connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } + } else { + val candidate = nameSearchCandidate(it.trim()) + connectNameCandidate.value = candidate + // clear the previous match immediately so the list falls back to text search during the debounce, + // instead of showing a stale filtered chat while the new search runs + searchChatFilteredBySimplexLink.value = emptySet() + if (candidate != null) { + // resolve the name locally on each keystroke, debounced; collectLatest cancels the in-flight + // search when the next keystroke arrives. A bare name can be a contact or a channel, so search + // both and filter every known chat found; drop the row only when both types are already known. + delay(NAME_SEARCH_DEBOUNCE_MS) + val rhId = chatModel.remoteHostId() + val inProgress = mutableStateOf(false) // background search: no spinner, no error alerts + val targets = if (candidate.startsWith("@") || candidate.startsWith("#")) listOf(candidate) else listOf("@$candidate", "#$candidate") + val ids = targets.mapNotNull { name -> + knownChatId(rhId, chatModel.controller.apiConnectPlan(rhId, name, PlanResolveMode.PRMNever, inProgress = inProgress)) } - } - null -> if (!searchShowingSimplexLink.value || it.isEmpty()) { + searchChatFilteredBySimplexLink.value = ids.toSet() + if (ids.size == targets.size) connectNameCandidate.value = null + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { if (it.isNotEmpty()) { focusRequester.requestFocus() } else { @@ -826,7 +834,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState } } searchShowingSimplexLink.value = false - searchChatFilteredBySimplexLink.value = null + searchChatFilteredBySimplexLink.value = emptySet() } } } @@ -837,13 +845,13 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState } } -private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState, cleanup: (() -> Unit)?) { +private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState>, cleanup: (() -> Unit)?) { withBGApi { planAndConnect( chatModel.remoteHostId(), link, - filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, - filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id }, + filterKnownContact = { searchChatFilteredBySimplexLink.value = setOf(it.id) }, + filterKnownGroup = { searchChatFilteredBySimplexLink.value = setOf(it.id) }, close = null, cleanup = cleanup, ) @@ -930,7 +938,8 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat // which is related to [derivedStateOf]. Using safe alternative instead // val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } } val searchShowingSimplexLink = remember { mutableStateOf(false) } - val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf>(emptySet()) } + val connectNameCandidate = remember { mutableStateOf(null) } val chats = filteredChats(searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList(), activeFilter.value) val topPaddingToContent = topPaddingToContent(false) val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent @@ -963,13 +972,23 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat if (oneHandUI.value) { Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { Divider() - TagsView(searchText) - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) + // bottom toolbar: search bar below, so on desktop the connect row goes below the tags + TagsOrConnectByName(searchText, connectNameCandidate) { candidate -> + TagsView(searchText) + Divider() + ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + } + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } else { - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - TagsView(searchText) + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate) + // top toolbar: search bar above, so on desktop the connect row goes above the tags + TagsOrConnectByName(searchText, connectNameCandidate) { candidate -> + ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + Divider() + TagsView(searchText) + } Divider() } } @@ -1017,6 +1036,105 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat } } +// Default top-level part used to complete a bare name typed in the search field (search field only; +// the message parser and the wire format are unchanged). +private const val DEFAULT_NAME_TLD = "testing" +// Shortest name that offers the button, so it is discoverable but does not flash on a single letter. +private const val MIN_NAME_LENGTH = 2 +// Wait this long after the last keystroke before the local name search runs. +internal const val NAME_SEARCH_DEBOUNCE_MS = 300L + +private val nameLabelRegex = Regex("[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*") +private fun isNameLabel(s: String): Boolean = s.length in 1..63 && nameLabelRegex.matches(s) + +// On-device candidate for connecting by SimpleX name: the string sent to the core to resolve it. +// Mirrors the domain grammar (nameLabelP/mkDomain in SimplexName.hs): an optional @/# prefix, then +// dot-separated ASCII labels; a dotless word is completed with the default top-level part. Returns +// the string to send (keeping @/# so the type is preserved), or null when the text is not a name. +internal fun nameSearchCandidate(str: String): String? { + val text = str.trim() + val prefix = text.firstOrNull()?.takeIf { it == '@' || it == '#' } + val core = if (prefix != null) text.substring(1) else text + val labels = core.split(".") + if (core.isEmpty() || labels.any { !isNameLabel(it) }) return null + return when { + labels.size > 1 -> text // already has a top-level part + core.length >= MIN_NAME_LENGTH -> "${prefix ?: ""}$core.$DEFAULT_NAME_TLD" + else -> null + } +} + +// The chat id a local (PRMNever) search resolved to — a contact, a business, or a channel — or null on a miss. +// The core returns the correct type for @ vs # (getContactToConnect / type-filtered getGroupToConnect), so no +// client-side type check is needed. +internal suspend fun knownChatId(rhId: Long?, result: ConnectionPlanResult?): String? = when (val plan = result?.connectionPlan) { + is ConnectionPlan.ContactAddress -> (plan.contactAddressPlan as? ContactAddressPlan.Known)?.contact?.let { contact -> + // a name-resolved chat may be prepared in the store but not yet listed, so add it (as the tap path does) + if (chatModel.getContactChat(contact.contactId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = emptyList())) + } + contact.id + } + is ConnectionPlan.GroupLink -> (when (val g = plan.groupLinkPlan) { + is GroupLinkPlan.Known -> g.groupInfo + is GroupLinkPlan.OwnLink -> g.groupInfo + else -> null + })?.let { gInfo -> + if (chatModel.getGroupChat(gInfo.groupId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(gInfo, groupChatScope = null), chatItems = emptyList())) + } + gInfo.id + } + else -> null +} + +// The list tags and the connect-by-name row share one slot. When there is no name, the tags show; on +// mobile the row replaces the tags while shown. On desktop both show, arranged by the caller (which +// knows whether the search bar is above or below), passed as desktopView. +@Composable +private fun TagsOrConnectByName( + searchText: MutableState, + connectNameCandidate: MutableState, + desktopView: @Composable (candidate: String) -> Unit, +) { + val candidate = connectNameCandidate.value + when { + candidate == null -> TagsView(searchText) + !appPlatform.isDesktop -> ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + else -> desktopView(candidate) + } +} + +@Composable +internal fun ConnectByNameRow(name: String, searchText: MutableState, connectNameCandidate: MutableState, close: (() -> Unit)?) { + val view = LocalMultiplatformView() + Row( + Modifier + .fillMaxWidth() + .clickable { + hideKeyboard(view) + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + name, + close = close, + cleanup = { + searchText.value = TextFieldValue() + connectNameCandidate.value = null + }, + ) + } + } + .padding(vertical = DEFAULT_PADDING_HALF), + verticalAlignment = Alignment.CenterVertically + ) { + // icon and text aligned with the search bar's icon and text (same paddings and icon size) + val icon = if (name.startsWith("@")) MR.images.ic_at else MR.images.ic_tag + Icon(painterResource(icon), null, Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.primary) + Text(String.format(generalGetString(MR.strings.connect_plan_connect_to_name), name), color = MaterialTheme.colors.primary) + } +} + @Composable private fun NoChatsView(searchText: MutableState) { val activeFilter = remember { chatModel.activeChatTagFilter }.value @@ -1324,14 +1442,14 @@ fun ItemPresetFilterAction( fun filteredChats( searchShowingSimplexLink: State, - searchChatFilteredBySimplexLink: State, + searchChatFilteredBySimplexLink: State>, searchText: String, chats: List, activeFilter: ActiveFilter? = null, ): List { - val linkChatId = searchChatFilteredBySimplexLink.value - return if (linkChatId != null) { - chats.filter { it.id == linkChatId } + val linkChatIds = searchChatFilteredBySimplexLink.value + return if (linkChatIds.isNotEmpty()) { + chats.filter { it.id in linkChatIds } } else { val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() if (s.isEmpty()) 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 96af5337d0..4e58501b19 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 @@ -197,7 +197,7 @@ private fun ShareList( 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 } - filteredChats(mutableStateOf(false), mutableStateOf(null), search, sorted) + filteredChats(mutableStateOf(false), mutableStateOf>(emptySet()), search, sorted) } } val topPaddingToContent = topPaddingToContent(false) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index b4680a4259..f70e4d0048 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -291,10 +291,13 @@ class AlertManager { profileFullName: String, profileImage: @Composable () -> Unit, profileBadge: LocalBadge? = null, + nameCaption: String? = null, subtitle: String? = null, information: String? = null, confirmText: String? = generalGetString(MR.strings.connect_plan_open_chat), onConfirm: (() -> Unit)? = null, + connectOtherButton: String? = null, + onConnectOther: (() -> Unit)? = null, dismissText: String = generalGetString(MR.strings.cancel_verb), onDismiss: (() -> Unit)? = null, ) { @@ -337,6 +340,17 @@ class AlertManager { modifier = Modifier.fillMaxWidth() ) + if (nameCaption != null) { + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text( + nameCaption, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary, + maxLines = 1, + modifier = Modifier.fillMaxWidth() + ) + } if (profileFullName.isNotEmpty() && profileFullName != profileName) { Spacer(Modifier.height(DEFAULT_PADDING_HALF)) Text( @@ -388,6 +402,14 @@ class AlertManager { Text(confirmText) } } + if (connectOtherButton != null && onConnectOther != null) { + TextButton(onClick = { + onConnectOther.invoke() + hideAlert() + }) { + Text(connectOtherButton) + } + } TextButton(onClick = { onDismiss?.invoke() hideAlert() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index a122ddd885..cc6b1c40a4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt @@ -36,7 +36,7 @@ fun SearchTextField( placeholder: String = stringResource(MR.strings.search_verb), enabled: Boolean = true, trailingContent: @Composable (() -> Unit)? = null, - reducedCloseButtonPadding: Dp = 0.dp, + reducedCloseButtonPadding: Dp = 8.dp, onValueChange: (String) -> Unit ) { val focusRequester = remember { FocusRequester() } @@ -116,7 +116,7 @@ fun SearchTextField( trailingIcon = if (searchText.value.text.isNotEmpty() || trailingContent != null) {{ Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.offset(x = 8.dp) + modifier = Modifier.offset(x = reducedCloseButtonPadding) ) { if (searchText.value.text.isNotEmpty()) { IconButton({ diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 3776a8d44d..161681c91d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -74,13 +74,19 @@ private suspend fun planAndConnectTask( cleanup?.invoke() completable.complete(!completable.isActive) } - val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, linkOwnerSig, inProgress = inProgress) + val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, linkOwnerSig = linkOwnerSig, inProgress = inProgress) connectProgressManager.stopConnectProgress() if (!inProgress.value) { return completable } if (result != null) { - val (connectionLink, connectionPlan) = result + val (connectionLink, planSimplexName, otherSimplexName, connectionPlan) = result val target = strConnectTarget(shortOrFullLink.trim()) val linkText = if (target is ConnectTarget.Link) "

${target.linkText}" else "" + // the name can also resolve to the other kind; its type picks the verb, its short form the label and target + val connectOtherLink = otherSimplexName?.shortStr + val connectOtherButton = otherSimplexName?.let { + val label = if (it.nameType == SimplexNameType.publicGroup) MR.strings.connect_plan_join_name else MR.strings.connect_plan_connect_to_name + generalGetString(label).format(it.shortStr) + } when (connectionPlan) { is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) { is InvitationLinkPlan.Ok -> @@ -154,7 +160,9 @@ private suspend fun planAndConnectTask( connectionLink, connectionPlan.contactAddressPlan.contactSLinkData_, ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, - verifiedDomain = connectionPlan.contactAddressPlan.verifiedDomain, + planSimplexName = planSimplexName, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, close, cleanup ) @@ -167,6 +175,8 @@ private suspend fun planAndConnectTask( connectDestructive = false, cleanup, ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } ContactAddressPlan.OwnLink -> { @@ -177,6 +187,8 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, connectDestructive = true, cleanup = cleanup, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } ContactAddressPlan.ConnectingConfirmReconnect -> { @@ -187,6 +199,8 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, connectDestructive = true, cleanup = cleanup, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } is ContactAddressPlan.ConnectingProhibit -> { @@ -195,7 +209,7 @@ private suspend fun planAndConnectTask( if (filterKnownContact != null) { filterKnownContact(contact) } else { - showOpenKnownContactAlert(chatModel, rhId, close, contact) + showOpenKnownContactAlert(chatModel, rhId, close, contact, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) cleanup() } } @@ -211,15 +225,24 @@ private suspend fun planAndConnectTask( if (filterKnownContact != null) { filterKnownContact(contact) } else { - showOpenKnownContactAlert(chatModel, rhId, close, contact) + showOpenKnownContactAlert(chatModel, rhId, close, contact, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) cleanup() } } is ContactAddressPlan.ContactViaAddress -> { Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress") val contact = connectionPlan.contactAddressPlan.contact - askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) - cleanup() + // the contact is already prepared in the store, so open the existing chat instead of sending a new + // connection request; surface it in the chat list first if it is not there yet (as for Known above) + if (chatModel.getContactChat(contact.contactId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = emptyList())) + } + if (filterKnownContact != null) { + filterKnownContact(contact) + } else { + showOpenKnownContactAlert(chatModel, rhId, close, contact, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) + cleanup() + } } } is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { @@ -232,7 +255,9 @@ private suspend fun planAndConnectTask( connectionPlan.groupLinkPlan.groupSLinkInfo_, connectionPlan.groupLinkPlan.groupSLinkData_, ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, - verifiedDomain = connectionPlan.groupLinkPlan.verifiedDomain, + planSimplexName = planSimplexName, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, close, cleanup ) @@ -245,6 +270,8 @@ private suspend fun planAndConnectTask( connectDestructive = false, cleanup = cleanup, ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } is GroupLinkPlan.OwnLink -> { @@ -253,7 +280,7 @@ private suspend fun planAndConnectTask( if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, connectionPlan, groupInfo, close, cleanup) + ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, connectionPlan, groupInfo, close, cleanup, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) } } GroupLinkPlan.ConnectingConfirmReconnect -> { @@ -264,6 +291,8 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, connectDestructive = true, cleanup = cleanup, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } is GroupLinkPlan.ConnectingProhibit -> { @@ -301,7 +330,7 @@ private suspend fun planAndConnectTask( if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - showOpenKnownGroupAlert(chatModel, rhId, close, groupInfo) + showOpenKnownGroupAlert(chatModel, rhId, close, groupInfo, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) cleanup() } } @@ -427,6 +456,8 @@ fun askCurrentOrIncognitoProfileAlert( connectDestructive: Boolean, cleanup: (() -> Unit)?, ownerVerification: OwnerVerification? = null, + connectOtherButton: String? = null, + connectOtherLink: String? = null, ) { val fullText = listOfNotNull(text, ownerVerificationMessage(ownerVerification)).joinToString("\n\n").ifEmpty { null } AlertManager.privacySensitive.showAlertDialogButtonsColumn( @@ -451,6 +482,14 @@ fun askCurrentOrIncognitoProfileAlert( }) { Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) } + if (connectOtherButton != null && connectOtherLink != null) { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { planAndConnect(rhId, connectOtherLink, close = close, cleanup = cleanup) } + }) { + Text(connectOtherButton, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } SectionItemView({ AlertManager.privacySensitive.hideAlert() cleanup?.invoke() @@ -473,7 +512,11 @@ fun openChat_(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, chat: Cha val alertProfileImageSize = 138.dp -private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) { +// For alerts that show the name inline (not as a profile with an avatar): "Alice" -> "Alice (@alice.testing)". +private fun nameWithDomain(name: String, planSimplexName: SimplexNameInfo?): String = + name + (planSimplexName?.let { " (${it.shortStr})" } ?: "") + +private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact, planSimplexName: SimplexNameInfo? = null, connectOtherButton: String? = null, connectOtherLink: String? = null) { AlertManager.privacySensitive.showOpenChatAlert( profileName = contact.profile.displayName, profileFullName = contact.profile.fullName, @@ -486,10 +529,13 @@ private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: }, // the alert shows the badge inline, so it skips the long-expired (ExpiredOld) badge here too profileBadge = if (contact.active && contact.profile.localBadge?.status != BadgeStatus.ExpiredOld) contact.profile.localBadge else null, + nameCaption = planSimplexName?.shortStr, confirmText = generalGetString(if (contact.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat), onConfirm = { openKnownContact(chatModel, rhId, close, contact) }, + connectOtherButton = connectOtherButton, + onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close) } } }, onDismiss = null ) } @@ -513,11 +559,14 @@ fun ownGroupLinkConfirmConnect( groupInfo: GroupInfo, close: (() -> Unit)?, cleanup: (() -> Unit)?, + planSimplexName: SimplexNameInfo? = null, + connectOtherButton: String? = null, + connectOtherLink: String? = null, ) { if (groupInfo.useRelays) { AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.connect_plan_this_is_your_link_for_channel), - text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_channel_vName), groupInfo.displayName), + text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_channel_vName), nameWithDomain(groupInfo.displayName, planSimplexName)), buttons = { Column { SectionItemView({ @@ -527,6 +576,14 @@ fun ownGroupLinkConfirmConnect( }) { Text(generalGetString(MR.strings.connect_plan_open_channel), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } + if (connectOtherButton != null && connectOtherLink != null) { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { planAndConnect(rhId, connectOtherLink, close = close, cleanup = cleanup) } + }) { + Text(connectOtherButton, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } SectionItemView({ AlertManager.privacySensitive.hideAlert() cleanup?.invoke() @@ -585,7 +642,7 @@ fun ownGroupLinkConfirmConnect( } } -private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) { +private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo, planSimplexName: SimplexNameInfo? = null, connectOtherButton: String? = null, connectOtherLink: String? = null) { val subscriberCount = if (groupInfo.useRelays) groupInfo.groupSummary.publicMemberCount?.let { subscriberCountStr(it) } else null AlertManager.privacySensitive.showOpenChatAlert( profileName = groupInfo.groupProfile.displayName, @@ -597,6 +654,7 @@ private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (( icon = groupInfo.chatIconName ) }, + nameCaption = planSimplexName?.shortStr, subtitle = subscriberCount, confirmText = generalGetString( if (groupInfo.useRelays) { @@ -610,6 +668,8 @@ private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (( onConfirm = { openKnownGroup(chatModel, rhId, close, groupInfo) }, + connectOtherButton = connectOtherButton, + onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close) } } }, onDismiss = null ) } @@ -629,7 +689,9 @@ fun showPrepareContactAlert( connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, ownerVerification: OwnerVerification? = null, - verifiedDomain: SimplexDomain? = null, + planSimplexName: SimplexNameInfo? = null, + connectOtherButton: String? = null, + connectOtherLink: String? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -647,13 +709,14 @@ fun showPrepareContactAlert( ) }, profileBadge = if (contactShortLinkData.localBadge?.status == BadgeStatus.ExpiredOld) null else contactShortLinkData.localBadge, + nameCaption = planSimplexName?.shortStr, information = ownerVerificationMessage(ownerVerification), confirmText = generalGetString(MR.strings.connect_plan_open_new_chat), onConfirm = { AlertManager.privacySensitive.hideAlert() ModalManager.closeAllModalsEverywhere() withBGApi { - val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData, verifiedDomain) + val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData, planSimplexName?.nameDomain) if (chat != null) { withContext(Dispatchers.Main) { ChatController.chatModel.chatsContext.addChat(chat) @@ -663,6 +726,8 @@ fun showPrepareContactAlert( cleanup?.invoke() } }, + connectOtherButton = connectOtherButton, + onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close, cleanup = cleanup) } } }, onDismiss = { cleanup?.invoke() } @@ -675,7 +740,9 @@ fun showPrepareGroupAlert( groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, ownerVerification: OwnerVerification? = null, - verifiedDomain: SimplexDomain? = null, + planSimplexName: SimplexNameInfo? = null, + connectOtherButton: String? = null, + connectOtherLink: String? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -691,6 +758,7 @@ fun showPrepareGroupAlert( icon = if (isChannel) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_supervised_user_circle_filled ) }, + nameCaption = planSimplexName?.shortStr, subtitle = subscriberCount, information = ownerVerificationMessage(ownerVerification), confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_new_group), @@ -698,7 +766,7 @@ fun showPrepareGroupAlert( AlertManager.privacySensitive.hideAlert() withBGApi { val directLink = groupShortLinkInfo?.direct ?: true - val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData, verifiedDomain) + val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData, planSimplexName?.nameDomain) if (chat != null) { withContext(Dispatchers.Main) { val relays = groupShortLinkInfo?.groupRelays @@ -715,6 +783,8 @@ fun showPrepareGroupAlert( cleanup?.invoke() } }, + connectOtherButton = connectOtherButton, + onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close, cleanup = cleanup) } } }, onDismiss = { cleanup?.invoke() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 41df280a8b..2419b11c6c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -136,7 +136,8 @@ private fun ModalData.NewChatSheetLayout( } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchShowingSimplexLink = remember { mutableStateOf(false) } - val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf>(emptySet()) } + val connectNameCandidate = remember { mutableStateOf(null) } val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value val baseContactTypes = remember { listOf(ContactType.CARD, ContactType.CONTACT_WITH_REQUEST, ContactType.REQUEST, ContactType.RECENT) } val contactTypes by remember(searchText.value.text.isEmpty()) { @@ -313,8 +314,13 @@ private fun ModalData.NewChatSheetLayout( searchText = searchText, searchShowingSimplexLink = searchShowingSimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + connectNameCandidate = connectNameCandidate, close = close, ) + connectNameCandidate.value?.let { candidate -> + Divider() + ConnectByNameRow(candidate, searchText, connectNameCandidate, close = close) + } Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } @@ -399,8 +405,13 @@ private fun ModalData.NewChatSheetLayout( searchText = searchText, searchShowingSimplexLink = searchShowingSimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + connectNameCandidate = connectNameCandidate, close = close, ) + connectNameCandidate.value?.let { candidate -> + Divider() + ConnectByNameRow(candidate, searchText, connectNameCandidate, close = close) + } Divider() } } @@ -466,7 +477,8 @@ private fun ContactsSearchBar( listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, - searchChatFilteredBySimplexLink: MutableState, + searchChatFilteredBySimplexLink: MutableState>, + connectNameCandidate: MutableState, close: () -> Unit, ) { var focused by remember { mutableStateOf(false) } @@ -485,6 +497,8 @@ private fun ContactsSearchBar( alwaysVisible = true, searchText = searchText, trailingContent = null, + // the clear button must line up with the filter icon it replaces, so no reduction here + reducedCloseButtonPadding = 0.dp, ) { searchText.value = searchText.value.copy(it) } @@ -523,34 +537,26 @@ private fun ContactsSearchBar( snapshotFlow { searchText.value.text } .distinctUntilChanged() .collect { - when (val target = strConnectTarget(it.trim())) { - is ConnectTarget.Link -> { - hideKeyboard(view) - searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero) - searchShowingSimplexLink.value = true - searchChatFilteredBySimplexLink.value = null - connect( - link = target.text, - searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, - close = close, - cleanup = { searchText.value = TextFieldValue() } - ) - } - is ConnectTarget.Name -> { - // A name lookup means "take me to this contact": open the chat if - // it's already known (visible prompt), unlike a pasted link which - // filters the list. So no filterKnownContact here. - hideKeyboard(view) - withBGApi { - planAndConnect( - chatModel.remoteHostId(), - target.text, - close = close, - cleanup = { searchText.value = TextFieldValue() }, - ) - } - } - null -> if (!searchShowingSimplexLink.value || it.isEmpty()) { + val target = strConnectTarget(it.trim()) + if (target is ConnectTarget.Link) { + hideKeyboard(view) + searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero) + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = emptySet() + connectNameCandidate.value = null + connect( + link = target.text, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + cleanup = { searchText.value = TextFieldValue() } + ) + } else { + // A name is resolved only when its "Connect to …" row is tapped, not on every keystroke. The + // simplex-name filter is chat-list only: this contacts/deleted view is a scoped subset, so a + // resolved chat id (channel, business, unlisted or active-only contact) may not be present in it. + val candidate = nameSearchCandidate(it.trim()) + connectNameCandidate.value = candidate + if (candidate == null && (!searchShowingSimplexLink.value || it.isEmpty())) { if (it.isNotEmpty()) { focusRequester.requestFocus() } else { @@ -560,7 +566,7 @@ private fun ContactsSearchBar( } } searchShowingSimplexLink.value = false - searchChatFilteredBySimplexLink.value = null + searchChatFilteredBySimplexLink.value = emptySet() } } } @@ -587,12 +593,12 @@ private fun ToggleFilterButton() { } } -private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState, close: () -> Unit, cleanup: (() -> Unit)?) { +private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState>, close: () -> Unit, cleanup: (() -> Unit)?) { withBGApi { planAndConnect( chatModel.remoteHostId(), link, - filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, + filterKnownContact = { searchChatFilteredBySimplexLink.value = setOf(it.id) }, close = close, cleanup = cleanup, ) @@ -602,15 +608,15 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState< private fun filteredContactChats( showUnreadAndFavorites: Boolean, searchShowingSimplexLink: State, - searchChatFilteredBySimplexLink: State, + searchChatFilteredBySimplexLink: State>, searchText: String, contactChats: List ): List { - val linkChatId = searchChatFilteredBySimplexLink.value + val linkChatIds = searchChatFilteredBySimplexLink.value val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() - return if (linkChatId != null) { - contactChats.filter { it.id == linkChatId } + return if (linkChatIds.isNotEmpty()) { + contactChats.filter { it.id in linkChatIds } } else { contactChats.filter { chat -> filterChat( @@ -666,7 +672,9 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats val listState = remember { appBarHandler.listState } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchShowingSimplexLink = remember { mutableStateOf(false) } - val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf>(emptySet()) } + // deleted contacts are not connected to by name, so this candidate only stops per-keystroke resolution + val connectNameCandidate = remember { mutableStateOf(null) } val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value val allChats by remember(chatModel.chats.value) { derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) } @@ -709,6 +717,7 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats searchText = searchText, searchShowingSimplexLink = searchShowingSimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + connectNameCandidate = connectNameCandidate, close = close, ) } else { @@ -718,6 +727,7 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats searchText = searchText, searchShowingSimplexLink = searchShowingSimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + connectNameCandidate = connectNameCandidate, close = close, ) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) 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 96c7226c36..6befeecb3b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -15,6 +15,8 @@ Connect Connect incognito Open chat + Join channel %s + Connect to %s Open new chat Open group Open new group diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index d1467d48fd..318fc210ee 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -560,8 +560,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName <> ".\nIt is hidden from the directory until approved." notifyAdminUsers $ "The " <> gt <> " " <> groupRef <> " is updated" <> byMember <> "." sendToApprove g' gr' n' - sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True Nothing) >>= \case - Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) -> + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) PRMAllGroups Nothing) >>= \case + Right (CRConnectionPlan _ _ _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) -> case dbOwnerMemberId gr of Just ownerGMId -> withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMember db (storeCxt cc) user groupId ownerGMId) >>= \case @@ -818,11 +818,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName forM_ pg_ $ \pg@PublicGroupProfile {groupLink} -> when (groupRegStatus == GRSActive || pendingApproval groupRegStatus) $ do let link = ACL SCMContact $ CLShort groupLink - sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True Nothing) >>= \case - Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated = BoolDef updated, linkOwners = ListDef owners}))) -> + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) PRMAllGroups Nothing) >>= \case + Right (CRConnectionPlan _ _ _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated, linkOwners = ListDef owners}))) -> checkValidOwner dbOwnerMemberId owners $ do - when updated $ reapprove pg gr groupRegStatus g' - when (updated || summary /= groupSummary g') $ listingsUpdated env + when groupUpdated $ reapprove pg gr groupRegStatus g' + when (groupUpdated || summary /= groupSummary g') $ listingsUpdated env Left (ChatErrorAgent {agentError = SMP _ err}) | linkDeleted err -> setGroupStatus logError st env cc groupId GRSRemoved $ \gr' -> notifyOwner gr' "The channel link is no longer valid.\nThe channel is removed from the directory." @@ -954,8 +954,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let link = ACL SCMContact $ CLShort connLink mId = MemberId oIdBytes gt' = groupTypeStr gt - sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True (Just ownerSig)) >>= \case - Right (CRConnectionPlan _ (ACCL SCMContact ccLink) plan) -> + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) PRMAllGroups (Just ownerSig)) >>= \case + Right (CRConnectionPlan _ (ACCL SCMContact ccLink) _ _ plan) -> handleGroupLinkPlan ct ccLink mId ownerSig gt' plan _ -> sendMessage cc ct "Error: could not connect. Please report it to directory admins." deChatLinkReceived ct (MCLGroup {groupProfile = GroupProfile {publicGroup = Just pg}}) _ = @@ -984,8 +984,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName (_, Just (OVFailed reason)) -> sendMessage cc ct $ "Link signature verification failed: " <> reason <> ".\nYou must be the " <> gt <> " owner to register it." (Nothing, _) -> sendMessage cc ct $ "Error: no " <> gt <> " information available via the link." _ -> sendMessage cc ct $ "Error: could not verify " <> gt <> " ownership. Please report it to directory admins." - GLPKnown {groupInfo = g, groupUpdated = BoolDef updated, ownerVerification} -> case ownerVerification of - Just OVVerified -> deReregistration ct g updated ownerSig + GLPKnown {groupInfo = g, groupUpdated, ownerVerification} -> case ownerVerification of + Just OVVerified -> deReregistration ct g groupUpdated ownerSig Just (OVFailed reason) -> sendMessage cc ct $ "Link signature verification failed: " <> reason <> ".\nYou must be the " <> gt <> " owner to register it." Nothing -> sendMessage cc ct $ "Error: could not verify " <> gt <> " ownership." GLPConnectingProhibit _ -> sendMessage cc ct $ "Already connecting to this " <> gt <> "." diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index 8dc57dbfc7..28ece08908 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -1370,7 +1370,7 @@ Determine SimpleX link type and if the bot is already connected via this link or **Parameters**: - userId: int64 - connectTarget: string? -- resolveKnown: bool +- resolveMode: [PlanResolveMode](./TYPES.md#planresolvemode) - linkOwnerSig: [LinkOwnerSig](./TYPES.md#linkownersig)? **Syntax**: @@ -1393,6 +1393,8 @@ ConnectionPlan: Connection link information. - type: "connectionPlan" - user: [User](./TYPES.md#user) - connLink: [CreatedConnLink](./TYPES.md#createdconnlink) +- planSimplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)? +- otherSimplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)? - connectionPlan: [ConnectionPlan](./TYPES.md#connectionplan) ChatCmdError: Command error (only used in WebSockets API). diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index d9dfa45961..ce5c834001 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -146,6 +146,7 @@ This file is generated automatically. - [OwnerVerification](#ownerverification) - [PaginationByTime](#paginationbytime) - [PendingContactConnection](#pendingcontactconnection) +- [PlanResolveMode](#planresolvemode) - [PrefEnabled](#prefenabled) - [Preferences](#preferences) - [PreparedContact](#preparedcontact) @@ -1116,6 +1117,9 @@ SimplexDomainNotReady: - simplexDomain: [SimplexDomain](#simplexdomain) - simplexDomainError: [SimplexDomainError](#simplexdomainerror) +NotResolvedLocally: +- type: "notResolvedLocally" + UnsupportedConnReq: - type: "unsupportedConnReq" @@ -1796,7 +1800,6 @@ Ok: - type: "ok" - contactSLinkData_: [ContactShortLinkData](#contactshortlinkdata)? - ownerVerification: [OwnerVerification](#ownerverification)? -- verifiedDomain: [SimplexDomain](#simplexdomain)? OwnLink: - type: "ownLink" @@ -2393,7 +2396,6 @@ Ok: - groupSLinkInfo_: [GroupShortLinkInfo](#groupshortlinkinfo)? - groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? - ownerVerification: [OwnerVerification](#ownerverification)? -- verifiedDomain: [SimplexDomain](#simplexdomain)? OwnLink: - type: "ownLink" @@ -3076,6 +3078,16 @@ count= - updatedAt: UTCTime +--- + +## PlanResolveMode + +**Enum type**: +- "allGroups" +- "unknown" +- "never" + + --- ## PrefEnabled diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index b28c71d8fd..35ea9fe261 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -328,6 +328,7 @@ chatTypesDocsData = (sti @NoteFolder, STRecord, "", [], "", ""), (sti @OwnerVerification, STUnion, "OV", [], "", ""), (sti @PendingContactConnection, STRecord, "", [], "", ""), + (sti @PlanResolveMode, STEnum, "PRM", [], "", ""), (sti @PrefEnabled, STRecord, "", [], "", ""), (sti @Preferences, STRecord, "", [], "", ""), (sti @PreparedContact, STRecord, "", [], "", ""), @@ -559,6 +560,7 @@ deriving instance Generic NewUser deriving instance Generic NoteFolder deriving instance Generic OwnerVerification deriving instance Generic PendingContactConnection +deriving instance Generic PlanResolveMode deriving instance Generic PrefEnabled deriving instance Generic Preferences deriving instance Generic PreparedContact diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index 606b09845d..31d6597b3f 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -500,7 +500,7 @@ export namespace APIAddContact { export interface APIConnectPlan { userId: number // int64 connectTarget?: string - resolveKnown: boolean + resolveMode: T.PlanResolveMode linkOwnerSig?: T.LinkOwnerSig } diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index 0fcf0e6eca..f54acfbeb1 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -186,6 +186,8 @@ export namespace CR { type: "connectionPlan" user: T.User connLink: T.CreatedConnLink + planSimplexName?: T.SimplexNameInfo + otherSimplexName?: T.SimplexNameInfo connectionPlan: T.ConnectionPlan } diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 8bd9e7797e..dcd9a5581f 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -1043,6 +1043,7 @@ export type ChatErrorType = | ChatErrorType.ChatStoreChanged | ChatErrorType.InvalidConnReq | ChatErrorType.SimplexDomainNotReady + | ChatErrorType.NotResolvedLocally | ChatErrorType.UnsupportedConnReq | ChatErrorType.ConnReqMessageProhibited | ChatErrorType.ContactNotReady @@ -1121,6 +1122,7 @@ export namespace ChatErrorType { | "chatStoreChanged" | "invalidConnReq" | "simplexDomainNotReady" + | "notResolvedLocally" | "unsupportedConnReq" | "connReqMessageProhibited" | "contactNotReady" @@ -1281,6 +1283,10 @@ export namespace ChatErrorType { simplexDomainError: SimplexDomainError } + export interface NotResolvedLocally extends Interface { + type: "notResolvedLocally" + } + export interface UnsupportedConnReq extends Interface { type: "unsupportedConnReq" } @@ -2049,7 +2055,6 @@ export namespace ContactAddressPlan { type: "ok" contactSLinkData_?: ContactShortLinkData ownerVerification?: OwnerVerification - verifiedDomain?: SimplexDomain } export interface OwnLink extends Interface { @@ -2681,7 +2686,6 @@ export namespace GroupLinkPlan { groupSLinkInfo_?: GroupShortLinkInfo groupSLinkData_?: GroupShortLinkData ownerVerification?: OwnerVerification - verifiedDomain?: SimplexDomain } export interface OwnLink extends Interface { @@ -3344,6 +3348,12 @@ export interface PendingContactConnection { updatedAt: string // ISO-8601 timestamp } +export enum PlanResolveMode { + AllGroups = "allGroups", + Unknown = "unknown", + Never = "never", +} + export interface PrefEnabled { forUser: boolean forContact: boolean diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py index 90f9801b42..9fb1a6c916 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py @@ -439,7 +439,7 @@ APIAddContact_Response = CR.Invitation | CR.ChatCmdError class APIConnectPlan(TypedDict): userId: int # int64 connectTarget: NotRequired[str] - resolveKnown: bool + resolveMode: "T.PlanResolveMode" linkOwnerSig: NotRequired["T.LinkOwnerSig"] diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py index e85de02c78..bd61e6dae1 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py @@ -57,6 +57,8 @@ class ConnectionPlan(TypedDict): type: Literal["connectionPlan"] user: "T.User" connLink: "T.CreatedConnLink" + planSimplexName: NotRequired["T.SimplexNameInfo"] + otherSimplexName: NotRequired["T.SimplexNameInfo"] connectionPlan: "T.ConnectionPlan" class ContactAlreadyExists(TypedDict): diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 4003260ad6..0856aaf772 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -793,6 +793,9 @@ class ChatErrorType_simplexDomainNotReady(TypedDict): simplexDomain: "SimplexDomain" simplexDomainError: "SimplexDomainError" +class ChatErrorType_notResolvedLocally(TypedDict): + type: Literal["notResolvedLocally"] + class ChatErrorType_unsupportedConnReq(TypedDict): type: Literal["unsupportedConnReq"] @@ -1024,6 +1027,7 @@ ChatErrorType = ( | ChatErrorType_chatStoreChanged | ChatErrorType_invalidConnReq | ChatErrorType_simplexDomainNotReady + | ChatErrorType_notResolvedLocally | ChatErrorType_unsupportedConnReq | ChatErrorType_connReqMessageProhibited | ChatErrorType_contactNotReady @@ -1080,7 +1084,7 @@ ChatErrorType = ( | ChatErrorType_exception ) -ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "simplexDomainNotReady", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"] +ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "simplexDomainNotReady", "notResolvedLocally", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"] ChatFeature = Literal["timedMessages", "fullDelete", "reactions", "voice", "files", "calls", "sessions"] @@ -1428,7 +1432,6 @@ class ContactAddressPlan_ok(TypedDict): type: Literal["ok"] contactSLinkData_: NotRequired["ContactShortLinkData"] ownerVerification: NotRequired["OwnerVerification"] - verifiedDomain: NotRequired["SimplexDomain"] class ContactAddressPlan_ownLink(TypedDict): type: Literal["ownLink"] @@ -1868,7 +1871,6 @@ class GroupLinkPlan_ok(TypedDict): groupSLinkInfo_: NotRequired["GroupShortLinkInfo"] groupSLinkData_: NotRequired["GroupShortLinkData"] ownerVerification: NotRequired["OwnerVerification"] - verifiedDomain: NotRequired["SimplexDomain"] class GroupLinkPlan_ownLink(TypedDict): type: Literal["ownLink"] @@ -2338,6 +2340,8 @@ class PendingContactConnection(TypedDict): createdAt: str # ISO-8601 timestamp updatedAt: str # ISO-8601 timestamp +PlanResolveMode = Literal["allGroups", "unknown", "never"] + class PrefEnabled(TypedDict): forUser: bool forContact: bool diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 50b862758b..48f4ec9eec 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -529,7 +529,7 @@ data ChatCommand | AddContact IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to - | APIConnectPlan {userId :: UserId, connectTarget :: Maybe AConnectTarget, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectTarget is used to report parsing failure as special error + | APIConnectPlan {userId :: UserId, connectTarget :: Maybe AConnectTarget, resolveMode :: PlanResolveMode, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectTarget is used to report parsing failure as special error | APIPrepareContact UserId ACreatedConnLink (Maybe SimplexDomain) ContactShortLinkData | APIPrepareGroup UserId CreatedLinkContact DirectLink (Maybe SimplexDomain) GroupShortLinkData | APIChangePreparedContactUser ContactId UserId @@ -670,6 +670,22 @@ data ChatCommand CustomChatCommand ByteString deriving (Show) +data PlanResolveMode + = PRMAllGroups -- resolve all known groups and all unknown chats + | PRMUnknown -- only resolve if chat is unknown (default) + | PRMNever -- do not resolve links and names, only do local search + deriving (Eq, Show) + +planResolveModeP :: A.Parser PlanResolveMode +planResolveModeP = + A.takeTill (== ' ') >>= \case + "allGroups" -> pure PRMAllGroups + "on" -> pure PRMAllGroups + "unknown" -> pure PRMUnknown + "off" -> pure PRMUnknown + "never" -> pure PRMNever + _ -> fail "bad PlanResolveMode" + allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal allowRemoteCommand = \case StartChat {} -> False @@ -820,7 +836,7 @@ data ChatResponse | CRInvitation {user :: User, connLinkInvitation :: CreatedLinkInvitation, connection :: PendingContactConnection} | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection, customUserProfile :: Maybe Profile} | CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User} - | CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, connectionPlan :: ConnectionPlan} + | CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, planSimplexName :: Maybe SimplexNameInfo, otherSimplexName :: Maybe SimplexNameInfo, connectionPlan :: ConnectionPlan} | CRNewPreparedChat {user :: User, chat :: AChat} | CRContactUserChanged {user :: User, fromContact :: Contact, newUser :: User, toContact :: Contact} | CRGroupUserChanged {user :: User, fromGroup :: GroupInfo, newUser :: User, toGroup :: GroupInfo} @@ -1096,7 +1112,7 @@ data InvitationLinkPlan deriving (Show) data ContactAddressPlan - = CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification, verifiedDomain :: Maybe SimplexDomain} + = CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification} | CAPOwnLink | CAPConnectingConfirmReconnect | CAPConnectingProhibit {contact :: Contact} @@ -1105,11 +1121,11 @@ data ContactAddressPlan deriving (Show) data GroupLinkPlan - = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification, verifiedDomain :: Maybe SimplexDomain} + = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification} | GLPOwnLink {groupInfo :: GroupInfo} | GLPConnectingConfirmReconnect | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} - | GLPKnown {groupInfo :: GroupInfo, groupUpdated :: BoolDef, ownerVerification :: Maybe OwnerVerification, linkOwners :: ListDef GroupLinkOwner} + | GLPKnown {groupInfo :: GroupInfo, groupUpdated :: Bool, ownerVerification :: Maybe OwnerVerification, linkOwners :: ListDef GroupLinkOwner} | GLPNoRelays {groupSLinkData_ :: Maybe GroupShortLinkData} | GLPUpdateRequired {groupSLinkData_ :: Maybe GroupShortLinkData} deriving (Show) @@ -1433,6 +1449,7 @@ data ChatErrorType | CEChatStoreChanged | CEInvalidConnReq | CESimplexDomainNotReady {simplexDomain :: SimplexDomain, simplexDomainError :: SimplexDomainError} + | CENotResolvedLocally -- a name or link is not a known chat in the local store and online resolution is off (PRMNever) | CEUnsupportedConnReq | CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String} | CEConnReqMessageProhibited diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 772e10dfb0..837317176f 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2058,8 +2058,9 @@ processChatCommand cxt nm = \case createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn deleteAgentConnectionAsync (aConnId' conn) pure conn' - APIConnectPlan userId (Just ct) resolveKnown linkOwnerSig_ -> withUserId userId $ \user -> - uncurry (CRConnectionPlan user) <$> connectPlan user ct resolveKnown linkOwnerSig_ + APIConnectPlan userId (Just ct) resolveMode linkOwnerSig_ -> withUserId userId $ \user -> do + (ccLink, planSimplexName, otherSimplexName, plan) <- connectPlan user ct resolveMode linkOwnerSig_ Nothing + pure $ CRConnectionPlan user ccLink planSimplexName otherSimplexName plan APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq APIPrepareContact userId accLink verifiedDomain contactSLinkData -> withUserId userId $ \user -> do let ContactShortLinkData {profile, message, business} = contactSLinkData @@ -2283,12 +2284,12 @@ processChatCommand cxt nm = \case CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq Connect incognito (Just ct) -> withUser $ \user -> do - let con m cReq = pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)) - (ccLink, plan) <- connectPlan user ct False Nothing `catchAllErrors` \e -> case ct of + let con m cReq = pure (ACCL m (CCLink cReq Nothing), Nothing, Nothing, CPInvitationLink (ILPOk Nothing Nothing)) + (ccLink, planSimplexName, otherSimplexName, plan) <- connectPlan user ct PRMUnknown Nothing Nothing `catchAllErrors` \e -> case ct of ACTarget m (CTFullContact cReq) -> con m cReq ACTarget m (CTInv (CLFull cReq)) -> con m cReq _ -> throwError e - connectWithPlan user incognito ccLink plan + connectWithPlan user incognito ccLink planSimplexName otherSimplexName plan Connect _ Nothing -> throwChatError CEInvalidConnReq APIVerifyContactDomain contactId -> withUser $ \user -> do ct@Contact {profile = LocalProfile {contactDomain}, preparedContact} <- withFastStore $ \db -> getContact db cxt user contactId @@ -2319,8 +2320,8 @@ processChatCommand cxt nm = \case toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e ConnectSimplex incognito -> withUser $ \user -> do - plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing Nothing)) - connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan + plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing)) + connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) Nothing Nothing plan DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing APIListContacts userId -> withUserId userId $ \user -> @@ -4178,13 +4179,13 @@ processChatCommand cxt nm = \case pure (gId, chatSettings) _ -> throwCmdError "not supported" processChatCommand cxt nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings - connectPlan :: User -> AConnectTarget -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) - connectPlan user (ACTarget SCMInvitation (CTInv cLink)) _ sig_ = case cLink of + connectPlan :: User -> AConnectTarget -> PlanResolveMode -> Maybe LinkOwnerSig -> Maybe (Either ChatError NameRecord) -> CM (ACreatedConnLink, Maybe SimplexNameInfo, Maybe SimplexNameInfo, ConnectionPlan) + connectPlan user (ACTarget SCMInvitation (CTInv cLink)) _ sig_ _ = case cLink of CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing Nothing CLShort l -> do let l' = serverShortLink l knownLinkPlans l' >>= \case - Just r -> pure r + Just (l, p) -> pure (l, Nothing, Nothing, p) Nothing -> do (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) @@ -4199,27 +4200,56 @@ processChatCommand cxt nm = \case Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db cxt user l' invitationReqAndPlan cReq sLnk_ cld ov = do plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError) - pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) - connectPlan user (ACTarget SCMContact ct) resolveKnown sig_ = case ct of + pure (ACCL SCMInvitation (CCLink cReq sLnk_), Nothing, Nothing, plan) + connectPlan user (ACTarget SCMContact ct) resolveMode sig_ nameRec = case ct of + CTDomain d + -- local search only: look up #d then @d in the store, without online name resolution + | resolveMode == PRMNever -> connectPlanNoName $ ChatError CENotResolvedLocally + | otherwise -> + tryAllErrors (withAgent $ \a -> resolveSimplexName a nm (aUserId user) d) >>= \case + Right nr + | isJust (firstNameLink CCTChannel (nrSimplexChannel nr)) -> + (addOther nr <$> connectPlanName NTPublicGroup (Right nr)) `catchAllErrors` \e -> + (addOther nr <$> connectPlanName NTContact (Right nr) `catchAllErrors` \_ -> throwError e) + | isJust (firstNameLink CCTContact (nrSimplexContact nr)) -> + addOther nr <$> connectPlanName NTContact (Right nr) + | otherwise -> connectPlanNoName $ ChatError $ CESimplexDomainNotReady d SDENoValidLink + Left e -> connectPlanNoName e + where + connectPlanName nameType nr_ = connectPlan user connTarget resolveMode sig_ (Just nr_) + where + connTarget = ACTarget SCMContact $ CTShortContact $ CTName $ SimplexNameInfo nameType d + connectPlanNoName e = + connectPlanName NTPublicGroup (Left e) `catchAllErrors` \e' -> + (connectPlanName NTContact (Left e) `catchAllErrors` \_ -> throwError e') + -- the same domain can resolve to both an @ name (contact or business) and a # channel; + -- keyed off the resolved name's type, so a contact name returning a business group still offers the channel + addOther nr (l, planName, _, p) = (l, planName, otherName, p) + where + otherName = case planName of + Just (SimplexNameInfo NTContact _) | isJust (firstNameLink CCTChannel (nrSimplexChannel nr)) -> Just $ SimplexNameInfo NTPublicGroup d + Just (SimplexNameInfo NTPublicGroup _) | isJust (firstNameLink CCTContact (nrSimplexContact nr)) -> Just $ SimplexNameInfo NTContact d + _ -> Nothing CTFullContact cReq -> do plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError) - pure (ACCL SCMContact $ CCLink cReq Nothing, plan) + pure (ACCL SCMContact $ CCLink cReq Nothing, Nothing, Nothing, plan) CTShortContact nl -> - case ctType of + (\(l, p) -> (l, simplexName_, Nothing, p)) <$> case ctType of CCTContact -> knownLinkPlans >>= \case Just r -> pure r Nothing -> do + when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally l' <- resolveSLink (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) let linkProfile_ = (\ContactShortLinkData {profile} -> profile) <$> contactSLinkData_ linkDomain_ = linkProfile_ >>= \Profile {contactDomain} -> claimDomain <$> contactDomain - verifiedDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing - refreshContact ct' = case (verifiedDomain, linkProfile_) of + planDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing + refreshContact ct' = case (planDomain, linkProfile_) of (Just _, Just p) -> updateContactFromLinkData user ct' p _ -> pure ct' - forM_ verifiedDomain $ \nameDomain -> + forM_ planDomain $ \nameDomain -> unless (linkDomain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain withFastStore' (\db -> getContactWithoutConnViaShortAddress db cxt user l') >>= \case Just ct' | not (contactDeleted ct') -> do @@ -4230,15 +4260,15 @@ processChatCommand cxt nm = \case ov = verifyLinkOwner rootKey owners l' sig_ plan <- contactRequestPlan user cReq contactSLinkData_ ov case plan of - CPContactAddress cap@(CAPOk {}) -> pure (con l' cReq, CPContactAddress cap {verifiedDomain}) CPContactAddress (CAPKnown ct') -> do ct'' <- refreshContact ct' - pure (con l' cReq, CPContactAddress (CAPKnown ct'')) + pure (con l' cReq, plan {contactAddressPlan = CAPKnown ct''}) CPContactAddress (CAPContactViaAddress ct') -> do ct'' <- refreshContact ct' - pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct'')) + pure (con l' cReq, plan {contactAddressPlan = CAPContactViaAddress ct''}) _ -> pure (con l' cReq, plan) where + knownLinkPlans :: CM (Maybe (ACreatedConnLink, ConnectionPlan)) knownLinkPlans = withFastStore $ \db -> liftIO (getUserContactLinkViaTarget db user nl') >>= \case Just UserContactLink {connLinkContact} -> pure $ Just (ACCL SCMContact connLinkContact, CPContactAddress CAPOwnLink) @@ -4250,24 +4280,26 @@ processChatCommand cxt nm = \case CCTChannel -> groupShortLinkPlan CCTRelay -> throwCmdError "chat relay links are not supported in this version" where - nl' = case nl of - CTLink sl -> CTLink (serverShortLink sl) - CTName _ -> nl + (nl', simplexName_) = case nl of + CTLink sl -> (CTLink (serverShortLink sl), Nothing) + CTName ni -> (nl, Just ni) ctType = case nl of CTLink (CSLContact _ t _ _) -> t CTName SimplexNameInfo {nameType = NTContact} -> CCTContact CTName SimplexNameInfo {nameType = NTPublicGroup} -> CCTChannel resolveSLink = case nl' of CTLink l' -> pure l' - CTName n -> serverShortLink <$> resolveNameLink user n + CTName n -> serverShortLink <$> resolveNameLink n con l' cReq = ACCL SCMContact $ CCLink cReq (Just l') - gPlan (ccl, g) = if memberRemoved (membership g) then Nothing else Just (ACCL SCMContact ccl, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef []))) + gPlan (ccl, g) = if memberRemoved (membership g) then Nothing else Just (ACCL SCMContact ccl, CPGroupLink (GLPKnown g False Nothing (ListDef []))) + groupShortLinkPlan :: CM (ACreatedConnLink, ConnectionPlan) groupShortLinkPlan = knownLinkPlans >>= \case Just (_, CPGroupLink (GLPKnown g _ _ _)) - | resolveKnown -> resolveKnownGroup g + | resolveMode == PRMAllGroups -> resolveKnownGroup g Just r -> pure r Nothing -> do + when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally l' <- resolveSLink (fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData @@ -4284,23 +4316,29 @@ processChatCommand cxt nm = \case (Nothing, Nothing) -> pure () _ -> throwChatError CEInvalidConnReq let ov = verifyLinkOwner rootKey owners l' sig_ - verifiedDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing - plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov - forM_ verifiedDomain $ \nameDomain -> + planDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing + plan0 <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov + -- a joined channel is found by link but not by name (its domain is not verified locally, + -- e.g. an un-upgraded relay dropped the claim); refresh its profile from the fresh link + -- data and mark it verified, so the check below passes and future by-name lookups match + plan <- case (planDomain, plan0, groupSLinkData_) of + (Just _, CPGroupLink (GLPKnown g u o os), Just sLinkData) -> + (\(g', _) -> CPGroupLink (GLPKnown g' u o os)) <$> updateGroupFromLinkData user g sLinkData + _ -> pure plan0 + forM_ planDomain $ \nameDomain -> let domain_ = (\GroupProfile {publicGroup} -> claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) =<< case plan of - CPGroupLink (GLPOk _ (Just GroupShortLinkData {groupProfile}) _ _) -> Just groupProfile + CPGroupLink (GLPOk _ (Just GroupShortLinkData {groupProfile}) _) -> Just groupProfile CPGroupLink (GLPKnown GroupInfo {groupProfile} _ _ _) -> Just groupProfile CPGroupLink (GLPOwnLink GroupInfo {groupProfile}) -> Just groupProfile CPGroupLink (GLPConnectingProhibit (Just GroupInfo {groupProfile})) -> Just groupProfile _ -> (\GroupShortLinkData {groupProfile} -> groupProfile) <$> groupSLinkData_ in unless (domain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain - pure $ case plan of - CPGroupLink glp@(GLPOk {}) -> (con l' cReq, CPGroupLink glp {verifiedDomain}) - _ -> (con l' cReq, plan) + pure (con l' cReq, plan) where unsupportedGroupType = \case Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel _ -> False + knownLinkPlans :: CM (Maybe (ACreatedConnLink, ConnectionPlan)) knownLinkPlans = withFastStore $ \db -> liftIO (getGroupInfoViaUserTarget db cxt user nl') >>= \case Just (ccl, g) -> pure $ Just (ACCL SCMContact ccl, CPGroupLink (GLPOwnLink g)) @@ -4314,29 +4352,29 @@ processChatCommand cxt nm = \case (g', updated) <- case groupSLinkData_ of Just sLinkData -> updateGroupFromLinkData user g sLinkData _ -> pure (g, False) - pure (con l' (linkConnReq fd), CPGroupLink (GLPKnown g' (BoolDef updated) ov (ListDef glOwners))) - -- resolve a name to its first contact/channel short link - resolveNameLink :: User -> SimplexNameInfo -> CM (ConnShortLink 'CMContact) - resolveNameLink user SimplexNameInfo {nameType, nameDomain} = do - NameRecord {nrSimplexContact, nrSimplexChannel} <- - withAgent $ \a -> resolveSimplexName a nm (aUserId user) nameDomain - let (candidates, ctType) = case nameType of - NTContact -> (nrSimplexContact, CCTContact) - NTPublicGroup -> (nrSimplexChannel, CCTChannel) - maybe (throwChatError $ CESimplexDomainNotReady nameDomain SDENoValidLink) pure $ firstNameLink ctType candidates - connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse - connectWithPlan user@User {userId} incognito ccLink plan + pure (con l' (linkConnReq fd), CPGroupLink (GLPKnown g' updated ov (ListDef glOwners))) + -- resolve a name to its first contact/channel short link + resolveNameLink :: SimplexNameInfo -> CM (ConnShortLink 'CMContact) + resolveNameLink SimplexNameInfo {nameType, nameDomain} = do + NameRecord {nrSimplexContact, nrSimplexChannel} <- maybe (withAgent $ \a -> resolveSimplexName a nm (aUserId user) nameDomain) (ExceptT . pure) nameRec + let (candidates, ctType') = case nameType of + NTContact -> (nrSimplexContact, CCTContact) + NTPublicGroup -> (nrSimplexChannel, CCTChannel) + maybe (throwChatError $ CESimplexDomainNotReady nameDomain SDENoValidLink) pure $ firstNameLink ctType' candidates + connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> Maybe SimplexNameInfo -> Maybe SimplexNameInfo -> ConnectionPlan -> CM ChatResponse + connectWithPlan user@User {userId} incognito ccLink planSimplexName otherSimplexName plan | connectionPlanProceed plan = do case plan of CPError e -> eToView e; _ -> pure () case plan of CPContactAddress (CAPContactViaAddress Contact {contactId}) -> processChatCommand cxt nm $ APIConnectContactViaAddress userId incognito contactId - CPContactAddress (CAPOk (Just sld) _ vName@(Just _)) -> connectContactViaName sld vName - CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _ vName) + CPContactAddress (CAPOk (Just sld) _) | isJust vName -> connectContactViaName sld vName + CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _) | ACCL SCMContact ccl <- ccLink -> joinChannelViaRelays ccl gld vName _ -> processChatCommand cxt nm $ APIConnect userId incognito $ Just ccLink - | otherwise = pure $ CRConnectionPlan user ccLink plan + | otherwise = pure $ CRConnectionPlan user ccLink planSimplexName otherSimplexName plan where + vName = nameDomain <$> planSimplexName joinChannelViaRelays :: CreatedLinkContact -> GroupShortLinkData -> Maybe SimplexDomain -> CM ChatResponse joinChannelViaRelays ccl gld vName = do GroupInfo {groupId} <- prepareChannelGroup @@ -4391,21 +4429,22 @@ processChatCommand cxt nm = \case contactRequestPlan user (CRContactUri crData) cld ov = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas + plan p = pure $ CPContactAddress p withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case - Just _ -> pure $ CPContactAddress CAPOwnLink + Just _ -> plan $ CAPOwnLink Nothing -> withFastStore' (\db -> getContactConnEntityByConnReqHash db cxt user cReqHashes) >>= \case Nothing -> withFastStore' (\db -> getContactWithoutConnViaAddress db cxt user cReqSchemas) >>= \case - Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct) - _ -> pure $ CPContactAddress (CAPOk cld ov Nothing) + Just ct | not (contactDeleted ct) -> plan $ CAPContactViaAddress ct + _ -> plan $ CAPOk cld ov Just (RcvDirectMsgConnection Connection {connStatus} Nothing) - | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk cld ov Nothing) - | otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect + | connStatus == ConnPrepared -> plan $ CAPOk cld ov + | otherwise -> plan CAPConnectingConfirmReconnect Just (RcvDirectMsgConnection _ (Just ct)) - | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) - | contactDeleted ct -> pure $ CPContactAddress (CAPOk cld ov Nothing) - | otherwise -> pure $ CPContactAddress (CAPKnown ct) + | not (contactReady ct) && contactActive ct -> plan $ CAPConnectingProhibit ct + | contactDeleted ct -> plan $ CAPOk cld ov + | otherwise -> plan $ CAPKnown ct -- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?) Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" @@ -4413,27 +4452,30 @@ processChatCommand cxt nm = \case groupJoinRequestPlan user (CRContactUri crData) linkInfo gld ov = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas + plan p = pure $ CPGroupLink p withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db cxt user cReqSchemas) >>= \case - Just g -> pure $ CPGroupLink (GLPOwnLink g) + Just g -> plan $ GLPOwnLink g Nothing -> do connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db cxt user cReqHashes gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db cxt user cReqHashes case (gInfo_, connEnt_) of - (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing) + (Nothing, Nothing) -> plan $ GLPOk linkInfo gld ov -- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?) - (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect + (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> plan $ GLPConnectingConfirmReconnect (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) - | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | otherwise -> pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing) + | not (contactReady ct) && contactActive ct -> plan $ GLPConnectingProhibit gInfo_ + | otherwise -> plan $ GLPOk linkInfo gld ov (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" (Just gInfo, _) -> groupPlan gInfo linkInfo gld ov groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan groupPlan gInfo@GroupInfo {membership} linkInfo gld ov - | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo (BoolDef False) ov (ListDef [])) + | memberStatus membership == GSMemRejected = plan $ GLPKnown gInfo False ov (ListDef []) | not (memberActive membership) && not (memberRemoved membership) = - pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) - | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo (BoolDef False) ov (ListDef [])) - | otherwise = pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing) + plan $ GLPConnectingProhibit $ Just gInfo + | memberActive membership = plan $ GLPKnown gInfo False ov (ListDef []) + | otherwise = plan $ GLPOk linkInfo gld ov + where + plan p = pure $ CPGroupLink p contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) contactCReqSchemas crData = ( CRContactUri crData {crScheme = SSSimplex}, @@ -5455,7 +5497,7 @@ chatCommandP = (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <* char_ '@' <*> (Just <$> displayNameP) <* A.space <*> quotedMsg <*> msgTextP), "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, - "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> ((" resolve=" *> onOffP) <|> pure False) <*> optional (" sig=" *> jsonP)), + "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> ((" resolve=" *> planResolveModeP) <|> pure PRMUnknown) <*> optional (" sig=" *> jsonP)), "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <*> optional (" domain=" *> strP) <* A.space <*> jsonP), "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <*> optional (" domain=" *> strP) <* A.space <*> jsonP), "/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal), diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 20d0d10292..c6c7251fc4 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1464,8 +1464,8 @@ updatePublicGroupData user gInfo | otherwise = pure gInfo updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> CM (GroupInfo, Bool) -updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} - | profileChanged || countChanged = do +updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupDomainVerified, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} + | profileChanged || countChanged || verifyChanged = do cxt <- chatStoreCxt withStore $ \db -> do g <- if profileChanged then updateGroupProfile db user gInfo groupProfile else pure gInfo @@ -1473,13 +1473,19 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = G Just PublicGroupData {publicMemberCount} | countChanged -> setPublicMemberCount db cxt user g publicMemberCount _ -> pure g - pure (g', profileChanged) + -- the group's own link is authoritative for its domain claim, so a claim in the link profile is + -- verified; updateGroupProfile above clears verification on a claim change, so set it afterwards + g'' <- if verifyChanged then liftIO $ setGroupDomainVerified db user g' True else pure g' + pure (g'', profileChanged) | otherwise = pure (gInfo, False) where profileChanged = p /= groupProfile countChanged = case publicGroupData of Just PublicGroupData {publicMemberCount} -> Just publicMemberCount /= localCount _ -> False + groupClaim GroupProfile {publicGroup} = claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim) + newClaim = groupClaim groupProfile + verifyChanged = isJust newClaim && (groupDomainVerified /= Just True || groupClaim p /= newClaim) updateContactFromLinkData :: User -> Contact -> Profile -> CM Contact updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {contactDomain = prevClaim, contactDomainVerified}} linkProfile@Profile {contactDomain = newClaim} diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 70e64f68d7..d6f51fadc3 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -238,7 +238,7 @@ import Simplex.Chat.Types.MemberRelations (IntroductionDirection (..), MemberRel import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), SimplexNameInfo (..), UserId) +import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), SimplexNameInfo (..), SimplexNameType (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) import qualified Simplex.FileTransfer.Description as FD import Simplex.Messaging.Encoding (smpDecode, smpEncode) @@ -1083,9 +1083,14 @@ getGroupToConnect :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> E getGroupToConnect db cxt user@User {userId} = \case CTLink sl -> first (`CCLink` Just sl) <$$> getGroupViaShortLinkToConnect db cxt user sl CTName ni -> - liftIO (maybeFirstRow id $ DB.query db byNameQuery (userId, nameDomain ni)) >>= \case - Just (gId :: Int64, Just cReq, Just (sLnk :: ShortLinkContact)) -> Just . (CCLink cReq (Just sLnk),) <$> getGroupInfo db cxt user gId - _ -> pure Nothing + -- @name is a business (presents as a contact); #name is a channel. The same domain can have both, + -- so the group type must match the requested name type. + let businessCond = case nameType ni of + NTContact -> " AND g.business_chat IS NOT NULL" + NTPublicGroup -> " AND g.business_chat IS NULL" + in liftIO (maybeFirstRow id $ DB.query db (byNameQuery <> businessCond) (userId, nameDomain ni)) >>= \case + Just (gId :: Int64, Just cReq, Just (sLnk :: ShortLinkContact)) -> Just . (CCLink cReq (Just sLnk),) <$> getGroupInfo db cxt user gId + _ -> pure Nothing where byNameQuery = [sql| diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 25f8f378d2..edf4aab33a 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -60,7 +60,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) -import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AConnectionLink (..), AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink (..), ConnectionMode (..), ConnectionModeI, ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), SConnectionMode (..), SimplexNameInfo, UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AConnectionLink (..), AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink (..), ConnectionMode (..), ConnectionModeI, ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), SConnectionMode (..), SimplexDomain, SimplexNameInfo (..), UserId) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) @@ -1789,6 +1789,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact data ConnectTarget (m :: ConnectionMode) where CTFullContact :: ConnectionRequestUri 'CMContact -> ConnectTarget 'CMContact CTShortContact :: ContactNameOrLink -> ConnectTarget 'CMContact + CTDomain :: SimplexDomain -> ConnectTarget 'CMContact CTInv :: ConnectionLink 'CMInvitation -> ConnectTarget 'CMInvitation data ContactNameOrLink = CTName SimplexNameInfo | CTLink (ConnShortLink 'CMContact) @@ -1813,10 +1814,12 @@ instance StrEncoding AConnectTarget where CTFullContact cr -> strEncode cr CTShortContact (CTName n) -> strEncode n CTShortContact (CTLink sl) -> strEncode sl + CTDomain d -> strEncode d CTInv l -> strEncode l strP = (ACTarget SCMContact . CTShortContact . CTName <$> (lookAhead nameStart *> strP)) <|> (aConnectTarget <$> strP) + <|> (ACTarget SCMContact . CTDomain <$> strP) where nameStart = "@" <|> "#" <|> "simplex:/name" diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 763cd1a963..0edfff6d29 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -204,7 +204,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation ccLink CRConnectionIncognitoUpdated u c customUserProfile -> ttyUser u $ viewConnectionIncognitoUpdated c customUserProfile testView CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' - CRConnectionPlan u connLink connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink connectionPlan + CRConnectionPlan u connLink _ otherSimplexName connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink connectionPlan <> otherSimplexNameNote otherSimplexName CRNewPreparedChat u (AChat _ (Chat cInfo _ _)) -> ttyUser u $ case cInfo of DirectChat ct -> [ttyContact' ct <> ": contact is prepared"] GroupChat g _ -> [ttyGroup' g <> ": group is prepared"] @@ -2144,6 +2144,12 @@ viewGroupUserChanged where userChangedStr = "group " <> ttyGroup' g <> " changed from user " <> plain un <> " to user " <> plain un' +otherSimplexNameNote :: Maybe SimplexNameInfo -> [StyledString] +otherSimplexNameNote = \case + Just ni@(SimplexNameInfo NTPublicGroup _) -> [plain $ "You can also join channel " <> shortNameInfoStr ni] + Just ni@(SimplexNameInfo NTContact _) -> [plain $ "You can also connect to " <> shortNameInfoStr ni <> " in direct chat"] + Nothing -> [] + viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> ConnectionPlan -> [StyledString] viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case CPInvitationLink ilp -> case ilp of @@ -2165,7 +2171,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("invitation link: " <>) CPContactAddress cap -> case cap of - CAPOk contactSLinkData ov _ -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] + CAPOk contactSLinkData ov -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] CAPOwnLink -> [ctAddr "own address"] CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"] CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] @@ -2183,7 +2189,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("contact address: " <>) CPGroupLink glp -> case glp of - GLPOk groupSLinkInfo_ groupSLinkData ov _ -> + GLPOk groupSLinkInfo_ groupSLinkData ov -> let direct = maybe True (\(GroupShortLinkInfo {direct = d}) -> d) groupSLinkInfo_ in [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"] <> viewSigVerification ov @@ -2687,6 +2693,7 @@ viewChatError isCmd logLevel testView = \case SDENoValidLink -> "has no valid connection link" SDEUnknownDomain -> "is not included in the connection link's profile" in [plain $ "SimpleX name " <> strEncode domain <> " " <> reason] + CENotResolvedLocally -> ["no matching chat found, name resolution is disabled"] CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr] CEInvalidChatMessage Connection {connId} msgMeta_ msg e -> [ plain $ diff --git a/tests/ChatTests/Names.hs b/tests/ChatTests/Names.hs index 7c1ef40155..cc46a65543 100644 --- a/tests/ChatTests/Names.hs +++ b/tests/ChatTests/Names.hs @@ -21,6 +21,9 @@ chatNamesTests = do it "connect by unregistered name fails to resolve" testConnectByNameNotFound it "set name not resolving to own address is rejected" testSetNameNotOwnAddress it "connect by channel name" testConnectByChannelName + it "connect by name resolving to channel (primary) and direct contact" testConnectByNameChannelAndContact + it "connect by name resolving to direct contact (primary) and channel" testConnectByNameContactAndChannel + it "connect by name resolving to business (primary) and channel" testConnectByNameBusinessAndChannel testConnectByName :: HasCallStack => TestParams -> IO () testConnectByName ps = withSmpServerAndNames $ \reg -> @@ -141,3 +144,82 @@ testConnectByChannelName ps = withSmpServerAndNames $ \reg -> bob <## "use #team to send messages" where teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" []) + +-- The bare name "team.simplex" resolves to both a channel and a direct contact. The channel is tried +-- first and succeeds (bob has joined #team), so it is the primary (planSimplexName); otherSimplexName +-- is the direct contact @team.simplex, shown as "You can also connect to @team.simplex in direct chat". +testConnectByNameChannelAndContact :: HasCallStack => TestParams -> IO () +testConnectByNameChannelAndContact ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + (channelLink, _) <- prepareChannel1Relay "team" alice cath + alice ##> "/ad" + (contactLink, _) <- getContactLinks alice True + registerName reg teamName (contactAndChannelNameRecord "team" (T.pack contactLink) (T.pack channelLink)) + alice ##> "/public group access #team domain=team.simplex" + alice <## "updated public group access: domain=team.simplex" + cath <## "alice updated group #team: (signed)" + cath <## "updated public group access: domain=team.simplex" + bob ##> "/c #team.simplex" + bob <## "#team: connection started" + concurrentlyN_ + [ bob + <### [ "#team: joining the group (connecting to relay cath)...", + "#team: you joined the group (connected to relay cath)" + ] + , do + cath <## "bob (Bob): accepting request to join group #team..." + cath <## "#team: bob joined the group" + , alice <### [EndsWith "introduced bob (Bob) in the channel"] + ] + bob ##> "/_connect plan 1 team.simplex" + bob <## "group link: known group #team" + bob <## "SimpleX name: #team (verified)" + bob <## "use #team to send messages" + bob <## "You can also connect to @team.simplex in direct chat" + where + teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" []) + +-- The bare name "acme.simplex" resolves to both a channel and a direct contact. The channel is tried +-- first but its group profile does not claim the domain, so the channel side of the plan fails; the +-- plan falls back to the direct contact as primary (planSimplexName) while otherSimplexName is the +-- channel #acme, shown as "You can also join channel #acme". The channel link is a real, fetchable +-- #acme channel, so the failure is the faithful "channel does not claim this domain" case, not a broken link. +testConnectByNameContactAndChannel :: HasCallStack => TestParams -> IO () +testConnectByNameContactAndChannel ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + (channelLink, _) <- prepareChannel1Relay "acme" alice cath + alice ##> "/ad" + (contactLink, _) <- getContactLinks alice True + registerName reg acmeName (contactAndChannelNameRecord "acme" (T.pack contactLink) (T.pack channelLink)) + alice ##> "/_set domain 1 acme.simplex" + alice <## "new contact address set" + bob ##> "/_connect plan 1 acme.simplex" + bob <## "contact address: ok to connect" + _ <- getTermLine bob -- contact short link data (JSON, printed in test view) + bob <## "You can also join channel #acme" + where + acmeName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "acme" []) + +testConnectByNameBusinessAndChannel :: HasCallStack => TestParams -> IO () +testConnectByNameBusinessAndChannel ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + (channelLink, _) <- prepareChannel1Relay "biz" alice cath + alice ##> "/ad" + (contactLink, _) <- getContactLinks alice True + registerName reg bizName (contactAndChannelNameRecord "biz" (T.pack contactLink) (T.pack channelLink)) + alice ##> "/auto_accept on business" + alice <## "auto_accept on, business" + alice ##> "/_set domain 1 biz.simplex" + alice <## "new contact address set" + bob ##> "/_connect plan 1 biz.simplex" + bob <## "business address: ok to connect" + _ <- getTermLine bob -- contact short link data (JSON, printed in test view) + bob <## "You can also join channel #biz" + where + bizName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "biz" []) diff --git a/tests/NameResolver.hs b/tests/NameResolver.hs index b7c0aaf2fc..35f5cd2496 100644 --- a/tests/NameResolver.hs +++ b/tests/NameResolver.hs @@ -11,6 +11,7 @@ module NameResolver registerName, contactNameRecord, channelNameRecord, + contactAndChannelNameRecord, resolverNamesConfig, ) where @@ -55,6 +56,11 @@ contactNameRecord name link = (emptyRecord name) {nrSimplexContact = [link]} channelNameRecord :: Text -> Text -> NameRecord channelNameRecord name link = (emptyRecord name) {nrSimplexChannel = [link]} +-- | A record whose domain resolves to both a direct contact link and a channel link. +contactAndChannelNameRecord :: Text -> Text -> Text -> NameRecord +contactAndChannelNameRecord name contactLink channelLink = + (emptyRecord name) {nrSimplexContact = [contactLink], nrSimplexChannel = [channelLink]} + emptyRecord :: Text -> NameRecord emptyRecord name = NameRecord From b38015c7b64fa2904ec1dd5464815a7a20e2a402 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 6 Jul 2026 08:55:01 +0100 Subject: [PATCH 21/21] core: 7.0.0.7 (simplex 7.0.0.4) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index fb8ac872d1..c66e079518 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a45d764eaa82fb4f26f8dfeb537c1464c8dad972 + tag: 551de8039fbfcbc75d8e2685a4c631a55ae28fb2 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 080a1ec635..15e7bb2b59 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a45d764eaa82fb4f26f8dfeb537c1464c8dad972" = "1f76904lwhxz9jkix0viaj715a88dlhr0x9v7jz79javcz57zcxp"; + "https://github.com/simplex-chat/simplexmq.git"."551de8039fbfcbc75d8e2685a4c631a55ae28fb2" = "1gn0fkxp60cizh2v7vjsd4c2bab2si7fl2bdzikisvg0na8ffnhw"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index e84e50bd0a..14bc3e4075 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 7.0.0.6 +version: 7.0.0.7 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat