From 80eb678892e7eaa84c61623f43789672ace264dd Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:31:12 +0000 Subject: [PATCH] ui: prefer selecting relays from different operators when creating channel (#6668) --- .../Shared/Views/NewChat/AddChannelView.swift | 43 ++++++++++++++++--- .../common/views/newchat/AddChannelView.kt | 40 ++++++++++++++--- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/apps/ios/Shared/Views/NewChat/AddChannelView.swift b/apps/ios/Shared/Views/NewChat/AddChannelView.swift index 66cfd0c7b9..d7709ea31c 100644 --- a/apps/ios/Shared/Views/NewChat/AddChannelView.swift +++ b/apps/ios/Shared/Views/NewChat/AddChannelView.swift @@ -165,7 +165,7 @@ struct AddChannelView: View { creationInProgress = true Task { do { - let enabledRelays = try await getEnabledRelays() + let enabledRelays = try await chooseRandomRelays() let relayIds = enabledRelays.compactMap { $0.chatRelayId } guard !relayIds.isEmpty else { await MainActor.run { @@ -201,13 +201,44 @@ struct AddChannelView: View { } } - // TODO [relays] move random relay selection to backend; prefer selecting relays from different operators - private func getEnabledRelays() async throws -> [UserChatRelay] { + private let maxRelays = 3 + + private func chooseRandomRelays() async throws -> [UserChatRelay] { let servers = try await getUserServers() - let all = servers.flatMap { op in - op.chatRelays.filter { $0.enabled && !$0.deleted && $0.chatRelayId != nil } + // Operator relays are grouped per operator; custom relays (nil operator) + // are treated independently to maximize trust distribution. + var operatorGroups: [[UserChatRelay]] = [] + var customRelays: [UserChatRelay] = [] + for op in servers { + let relays = op.chatRelays.filter { $0.enabled && !$0.deleted && $0.chatRelayId != nil } + guard !relays.isEmpty else { continue } + if op.operator != nil { + operatorGroups.append(relays.shuffled()) + } else { + customRelays = relays.shuffled() + } } - return Array(all.shuffled().prefix(3)) + var selected: [UserChatRelay] = [] + // Prefer at least one custom relay when available - + // user's own infrastructure for trust distribution. + if let relay = customRelays.first { + selected.append(relay) + customRelays.removeFirst() + if selected.count >= maxRelays { return selected } + } + // Round-robin across shuffled groups to distribute relays across operators. + var groups = operatorGroups + customRelays.map { [$0] } + groups.shuffle() + let maxDepth = groups.map(\.count).max() ?? 0 + for depth in 0..= maxRelays { return selected } + } + } + } + return selected } private func checkHasRelays() async -> Bool { 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 c5085586ba..fc5b2f0640 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 @@ -115,7 +115,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit creationInProgress.value = true withBGApi { try { - val enabledRelays = getEnabledRelays() + val enabledRelays = chooseRandomRelays() val relayIds = enabledRelays.mapNotNull { it.chatRelayId } if (relayIds.isEmpty()) { withContext(Dispatchers.Main) { @@ -159,12 +159,42 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit } } -private suspend fun getEnabledRelays(): List { +private const val maxRelays = 3 + +private suspend fun chooseRandomRelays(): List { val servers = getUserServers(rh = null) ?: return emptyList() - val all = servers.flatMap { op -> - op.chatRelays.filter { it.enabled && !it.deleted && it.chatRelayId != null } + // Operator relays are grouped per operator; custom relays (null operator) + // are treated independently to maximize trust distribution. + val operatorGroups = mutableListOf>() + var customRelays = mutableListOf() + for (op in servers) { + val relays = op.chatRelays.filter { it.enabled && !it.deleted && it.chatRelayId != null } + if (relays.isEmpty()) continue + if (op.operator != null) { + operatorGroups.add(relays.shuffled()) + } else { + customRelays = relays.shuffled().toMutableList() + } } - return all.shuffled().take(3) + val selected = mutableListOf() + // Prefer at least one custom relay when available - + // user's own infrastructure for trust distribution. + if (customRelays.isNotEmpty()) { + selected.add(customRelays.removeAt(0)) + if (selected.size >= maxRelays) return selected + } + // Round-robin across shuffled groups to distribute relays across operators. + val groups = (operatorGroups + customRelays.map { listOf(it) }).shuffled() + val maxDepth = groups.maxOfOrNull { it.size } ?: 0 + for (depth in 0 until maxDepth) { + for (group in groups) { + if (depth < group.size) { + selected.add(group[depth]) + if (selected.size >= maxRelays) return selected + } + } + } + return selected } private suspend fun checkHasRelays(): Boolean {