From f48377a3fa4922fcc0abdc6d095926f8d1da48b3 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:30:54 +0000 Subject: [PATCH] ios: review fixes for wrong QR code recognition Follow-up to "Recognise wrong QR code type in every scanner", from an iOS review of that commit. - ScanProtocolServer: a rejected scan now alerts in place instead of dismissing, but the sheet had no way out - move its title into the nav bar and add a Cancel button. Fixes the preview, which needs AppTheme now that the themed background is applied inside the view. - MigrateToDevice: classify the pasted link off the main thread, as the Kotlin paste path already does - pasted text is unbounded and classifying a file link parses its YAML description. Guard the result, as the sheet can be dismissed while classifying. - Relay message named the Chat relay screen but not that the address is only kept once the relay is tested, so following it lost the pasted address to an "Invalid relay name!" alert. The migration message named a label that only exists on desktop. - ContentView: the /r deep link showed the relay alert without the guidance added to the other call site; both literals were identical before. - Document WrongQRCode.swift per apps/ios/CODE.md, and refresh the specs and product docs that quote the changed messages. --- apps/ios/CODE.md | 1 + apps/ios/Shared/ContentView.swift | 2 +- .../Views/Migration/MigrateToDevice.swift | 21 ++++++++++------- .../Shared/Views/NewChat/NewChatView.swift | 2 +- .../Shared/Views/NewChat/WrongQRCode.swift | 16 +++---------- .../ScanProtocolServer.swift | 23 +++++++++++-------- apps/ios/SimpleXChat/API.swift | 12 +++++----- apps/ios/product/flows/connection.md | 2 +- apps/ios/product/views/chat-list.md | 2 +- apps/ios/product/views/new-chat.md | 2 +- apps/ios/spec/architecture.md | 6 ++--- apps/ios/spec/client/navigation.md | 15 +++++++++--- apps/ios/spec/impact.md | 1 + .../commonMain/resources/MR/base/strings.xml | 6 ++--- 14 files changed, 60 insertions(+), 51 deletions(-) diff --git a/apps/ios/CODE.md b/apps/ios/CODE.md index 5a8356f656..0aaa3a8136 100644 --- a/apps/ios/CODE.md +++ b/apps/ios/CODE.md @@ -178,6 +178,7 @@ After completing all changes (code + documentation), you MUST run an adversarial | Shared/Views/Chat/Group/ChannelRelaysView.swift | spec/client/chat-view.md | product/views/group-info.md | | Shared/Views/NewChat/NewChatView.swift | spec/client/navigation.md | product/views/new-chat.md | | Shared/Views/NewChat/QRCode.swift | spec/client/navigation.md | product/views/new-chat.md | +| Shared/Views/NewChat/WrongQRCode.swift | spec/client/navigation.md | product/views/new-chat.md | | Shared/Views/Call/ActiveCallView.swift | spec/services/calls.md | product/views/call.md | | Shared/Views/Call/CallController.swift | spec/services/calls.md | product/flows/calling.md | | Shared/Views/Call/WebRTCClient.swift | spec/services/calls.md | product/flows/calling.md | diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 22d0da3829..e9feec224a 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -466,7 +466,7 @@ struct ContentView: View { if path == "/r" { showAlert( NSLocalizedString("Relay address", comment: "alert title"), - message: NSLocalizedString("This is a chat relay address, it cannot be used to connect.", comment: "alert message") + message: NSLocalizedString("This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.", comment: "alert message") ) } else if (path == "/contact" || path == "/invitation" || path == "/a" || path == "/c" || path == "/g" || path == "/i") { path.removeFirst() diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index 992c4d821a..d8ab1528c0 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -211,7 +211,6 @@ struct MigrateToDevice: View { case nil: alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") case let type?: - // shared with the paste button, so the title is neutral ("Wrong link") alert = .error(title: "Wrong link", error: wrongQRCodeMessage(type)) } case let .failure(e): @@ -237,13 +236,19 @@ struct MigrateToDevice: View { Button { if let str = UIPasteboard.general.string { let trimmed = str.trimmingCharacters(in: .whitespacesAndNewlines) - switch checkLink(trimmed) { - case .some(.fileDescription): - migrationState = .linkDownloading(link: trimmed) - case nil: - alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") - case let type?: - alert = .error(title: "Wrong link", error: wrongQRCodeMessage(type)) + Task { + let linkType = await checkLinkAsync(trimmed) + // the sheet can be dismissed while classifying; applying the result then + // resurrects it (SimpleXInfo) or skips the paste screen (CreateProfile) + guard case .some(.pasteOrScanLink) = migrationState else { return } + switch linkType { + case .some(.fileDescription): + migrationState = .linkDownloading(link: trimmed) + case nil: + alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.") + case let type?: + alert = .error(title: "Wrong link", error: wrongQRCodeMessage(type)) + } } } } label: { diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index b40bbccdad..ac52c57ef7 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -1327,7 +1327,7 @@ func planAndConnect( if linkType == .relay { showAlert( NSLocalizedString("Relay address", comment: "alert title"), - message: NSLocalizedString("This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there.", comment: "alert message") + message: NSLocalizedString("This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.", comment: "alert message") ) cleanup?() return diff --git a/apps/ios/Shared/Views/NewChat/WrongQRCode.swift b/apps/ios/Shared/Views/NewChat/WrongQRCode.swift index 55f676ee72..46597ad9ab 100644 --- a/apps/ios/Shared/Views/NewChat/WrongQRCode.swift +++ b/apps/ios/Shared/Views/NewChat/WrongQRCode.swift @@ -2,31 +2,23 @@ // WrongQRCode.swift // SimpleX // -// iOS mirror of the Kotlin WrongQRCode.kt (apps/multiplatform/.../views/newchat/WrongQRCode.kt). -// -// NOTE: written without an Xcode/macOS toolchain and NOT compiled here — build/verify on CI or a Mac. -// import Foundation import SwiftUI import SimpleXChat -// Message shown when a scanner (or the Migrate paste field) is handed a valid -// SimpleX code of a kind it does not accept: name what it actually is and where -// to use it. The type comes from the core classifier (checkLink), so this is -// purely presentation — no parsing here. iOS uses %@ (not %s) for the substitution. func wrongQRCodeMessage(_ type: ScannedLinkType) -> String { switch type { case let .connection(linkType): if linkType == .relay { - return String.localizedStringWithFormat(NSLocalizedString("This is a %@. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there.", comment: "wrong QR code alert"), linkType.description) + return String.localizedStringWithFormat(NSLocalizedString("This is a %@. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.", comment: "wrong QR code alert"), linkType.description) } else { return String.localizedStringWithFormat(NSLocalizedString("This is a %@. To use it, open New chat, then scan or paste it there.", comment: "wrong QR code alert"), linkType.description) } case .server: return NSLocalizedString("This is a SimpleX server address. To use it, open Network & servers, Your servers, Add server, then Scan server QR code.", comment: "wrong QR code alert") case .fileDescription: - return NSLocalizedString("This is a link to migrate to another device. To use it, when setting up a new device, choose to migrate from another device.", comment: "wrong QR code alert") + return NSLocalizedString("This is a link to migrate to another device. To use it, choose Migrate when setting up a new device.", comment: "wrong QR code alert") case .desktopCtrl: return NSLocalizedString("This is an address to connect to a desktop app. To use it, open Use from desktop and scan the QR code shown in the desktop app.", comment: "wrong QR code alert") case .verificationCode: @@ -34,9 +26,7 @@ func wrongQRCodeMessage(_ type: ScannedLinkType) -> String { } } -// The shared "Wrong QR code" alert. Callers present it through their own alert -// state so it appears within the scanner's presentation (a global/root alert -// may not show over a modal scanner). +// Returned rather than shown, because a global/root alert may not appear over a modal scanner. func wrongQRCodeAlert(_ message: String) -> Alert { Alert(title: Text("Wrong QR code"), message: Text(verbatim: message)) } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift index b21f708917..d9699a89e8 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift @@ -19,19 +19,22 @@ struct ScanProtocolServer: View { @State private var scanAlert: SomeAlert? var body: some View { - VStack(alignment: .leading) { - Text("Scan server QR code") - .font(.largeTitle) - .bold() - .padding(.vertical) + NavigationView { CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) - .padding(.top) + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .navigationTitle("Scan server QR code") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + } + .alert(item: $scanAlert) { $0.alert } } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .alert(item: $scanAlert) { $0.alert } } func processQRCode(_ resp: Result) { @@ -44,7 +47,6 @@ struct ScanProtocolServer: View { server.server = trimmed addServer(server, $userServers, $serverErrors, $serverWarnings, dismiss) case nil: - // unrecognised: scanner-local alert (deliberate iOS change from the pre-PR dismiss-then-global flow) scanAlert = SomeAlert( alert: mkAlert(title: "Invalid server address!", message: "Check server address and try again."), id: "invalidServerAddress" @@ -66,5 +68,6 @@ struct ScanProtocolServer_Previews: PreviewProvider { serverErrors: Binding.constant([]), serverWarnings: Binding.constant([]) ) + .environmentObject(CurrentColors.toAppTheme()) } } diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 85153e1cea..554c485c9f 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -187,10 +187,6 @@ struct ParsedServerAddress: Decodable { var parseError: String } -// The kind of SimpleX QR code / link a scanned string turned out to be, as -// determined by the core (chat_check_link). Public: it crosses the -// SimpleXChat -> app module boundary. Case names and the connection payload -// label must match the Haskell ScannedLinkType wire tags. public enum ScannedLinkType: Decodable { case connection(linkType: SimplexLinkType) case server @@ -199,8 +195,6 @@ public enum ScannedLinkType: Decodable { case verificationCode } -// Wrapper stays internal — it never leaves the framework. A missing linkType -// key (the "not a SimpleX code" case) decodes to nil. struct CheckedLink: Decodable { var linkType: ScannedLinkType? } @@ -219,6 +213,12 @@ public func checkLink(_ s: String) -> ScannedLinkType? { return nil } +// nonisolated async, so callers on the main actor offload to the global executor: +// pasted text is unbounded, and classifying a file link parses its YAML description +public func checkLinkAsync(_ s: String) async -> ScannedLinkType? { + checkLink(s) +} + public func parseSanitizeUri(_ s: String, safe: Bool) -> ParsedUri? { var c = s.cString(using: .utf8)! if let cjson = chat_parse_uri(&c, safe ? 1 : 0) { diff --git a/apps/ios/product/flows/connection.md b/apps/ios/product/flows/connection.md index 7073e07070..c24b8fe800 100644 --- a/apps/ios/product/flows/connection.md +++ b/apps/ios/product/flows/connection.md @@ -104,7 +104,7 @@ Establishing contact between two SimpleX Chat users. SimpleX uses no user identi ### 7a. Relay Link Rejection 1. User scans, pastes, or opens a relay address link (URL path `/r` or `SimplexLinkType.relay`). -2. In `ContentView.connectViaUrl_()`: early return with alert "Relay address" / "This is a chat relay address, it cannot be used to connect." +2. In `ContentView.connectViaUrl_()`: intercepted with alert "Relay address" / "This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay." 3. In `NewChatView.planAndConnect()`: `.simplexLink(_, .relay, _, _)` pattern triggers the same alert. 4. The link is NOT processed further. No connection is attempted. diff --git a/apps/ios/product/views/chat-list.md b/apps/ios/product/views/chat-list.md index 04d19bef9e..39fbb39edf 100644 --- a/apps/ios/product/views/chat-list.md +++ b/apps/ios/product/views/chat-list.md @@ -81,7 +81,7 @@ When a group has `groupInfo.useRelays == true` (channel): ### Relay URL Handling -When a relay address link (`/r` path) is opened via URL deep link, `ContentView.connectViaUrl_()` intercepts it and shows an alert: "Relay address" / "This is a chat relay address, it cannot be used to connect." The link is not processed further. +When a relay address link (`/r` path) is opened via URL deep link, `ContentView.connectViaUrl_()` intercepts it and shows an alert: "Relay address" / "This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay." The link is not processed further. ### Swipe Actions diff --git a/apps/ios/product/views/new-chat.md b/apps/ios/product/views/new-chat.md index 0d1e384325..b262cd86d2 100644 --- a/apps/ios/product/views/new-chat.md +++ b/apps/ios/product/views/new-chat.md @@ -111,7 +111,7 @@ Calls `apiNewPublicGroup(incognito:relayIds:groupProfile:)` which returns `publi ### Relay Link Blocking -When `planAndConnect` encounters a `.simplexLink(_, .relay, _, _)`, it shows a "Relay address" alert: "This is a chat relay address, it cannot be used to connect." Connection is blocked. +When `planAndConnect` encounters a `.simplexLink(_, .relay, _, _)`, it shows a "Relay address" alert: "This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay." Connection is blocked. ### Channel Prepare/Join Alerts diff --git a/apps/ios/spec/architecture.md b/apps/ios/spec/architecture.md index 8e651a0433..8b7ff35f22 100644 --- a/apps/ios/spec/architecture.md +++ b/apps/ios/spec/architecture.md @@ -5,7 +5,7 @@ > Related specs: [README](README.md) | [API Reference](api.md) | [State Management](state.md) | [Database](database.md) > Related product: [Product Overview](../product/README.md) -**Source:** [`SimpleXApp.swift`](../Shared/SimpleXApp.swift#L1-L183) | [`AppDelegate.swift`](../Shared/AppDelegate.swift#L1-L209) | [`ContentView.swift`](../Shared/ContentView.swift#L1-L513) | [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1373) | [`SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L1-L2915) | [`AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L1-L2357) | [`APITypes.swift`](../SimpleXChat/APITypes.swift#L1-L1071) | [`API.swift`](../SimpleXChat/API.swift#L1-L388) +**Source:** [`SimpleXApp.swift`](../Shared/SimpleXApp.swift#L1-L183) | [`AppDelegate.swift`](../Shared/AppDelegate.swift#L1-L209) | [`ContentView.swift`](../Shared/ContentView.swift#L1-L513) | [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1373) | [`SimpleXAPI.swift`](../Shared/Model/SimpleXAPI.swift#L1-L2915) | [`AppAPITypes.swift`](../Shared/Model/AppAPITypes.swift#L1-L2357) | [`APITypes.swift`](../SimpleXChat/APITypes.swift#L1-L1071) | [`API.swift`](../SimpleXChat/API.swift#L1-L429) --- @@ -255,7 +255,7 @@ The Share Extension (`SimpleX SE/`) allows sharing content (text, images, files) --- -## [7. Remote Desktop Control](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545) +## [7. Remote Desktop Control](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L556) Optional desktop pairing allows controlling the mobile app from a desktop client: @@ -263,7 +263,7 @@ Optional desktop pairing allows controlling the mobile app from a desktop client - **Commands**: [`connectRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1613), [`findKnownRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1620), [`confirmRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1624), [`verifyRemoteCtrlSession`](../Shared/Model/SimpleXAPI.swift#L1630), [`listRemoteCtrls`](../Shared/Model/SimpleXAPI.swift#L1636), [`stopRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1642), [`deleteRemoteCtrl`](../Shared/Model/SimpleXAPI.swift#L1646) - **State**: [`ChatModel.remoteCtrlSession`](../Shared/Model/ChatModel.swift#L395)`: RemoteCtrlSession?` tracks the active session - **Transport**: Encrypted reverse HTTP transport between mobile and desktop -- **Source**: [`Shared/Views/RemoteAccess/ConnectDesktopView.swift`](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L545), see `Remote.hs` in `../../src/Simplex/Chat/` +- **Source**: [`Shared/Views/RemoteAccess/ConnectDesktopView.swift`](../Shared/Views/RemoteAccess/ConnectDesktopView.swift#L1-L556), see `Remote.hs` in `../../src/Simplex/Chat/` --- diff --git a/apps/ios/spec/client/navigation.md b/apps/ios/spec/client/navigation.md index 64d0940e39..386fbb1543 100644 --- a/apps/ios/spec/client/navigation.md +++ b/apps/ios/spec/client/navigation.md @@ -329,14 +329,22 @@ In `connectViaUrl_()`, relay address links (URL path `/r`) are intercepted befor ```swift if path == "/r" { showAlert(NSLocalizedString("Relay address", ...), - message: NSLocalizedString("This is a chat relay address, it cannot be used to connect.", ...)) - return + message: NSLocalizedString("This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.", ...)) } ``` Similarly, in `planAndConnect()` (`NewChatView.swift`), `.simplexLink(_, .relay, _, _)` patterns trigger the same alert and block connection. -## 11. Channel-Specific NewChatView Behavior +## 11. Scanned Link Classification + +**Source:** [`Shared/Views/NewChat/WrongQRCode.swift`](../../Shared/Views/NewChat/WrongQRCode.swift) + +Each scanner, and the Migrate-to-device paste button, routes its input through +`checkLink()` first. A recognised code of the wrong kind is reported by +`wrongQRCodeMessage(_:)` / `wrongQRCodeAlert(_:)`, which name what the code is and +where to use it; an unrecognised string falls back to that scanner's own error. + +## 12. Channel-Specific NewChatView Behavior **Source:** [`Shared/Views/NewChat/NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift) @@ -372,6 +380,7 @@ For channels (`groupInfo.useRelays`): the title is "Open channel"; for groups, " | New chat view | [`Shared/Views/NewChat/NewChatView.swift`](../../Shared/Views/NewChat/NewChatView.swift) | | Channel creation | [`Shared/Views/NewChat/AddChannelView.swift`](../../Shared/Views/NewChat/AddChannelView.swift) | | New chat menu | [`Shared/Views/NewChat/NewChatMenuButton.swift`](../../Shared/Views/NewChat/NewChatMenuButton.swift) | +| Wrong QR code alert | [`Shared/Views/NewChat/WrongQRCode.swift`](../../Shared/Views/NewChat/WrongQRCode.swift) | | Settings view | [`Shared/Views/UserSettings/SettingsView.swift`](../../Shared/Views/UserSettings/SettingsView.swift) | | User profiles | [`Shared/Views/UserSettings/UserProfilesView.swift`](../../Shared/Views/UserSettings/UserProfilesView.swift) | | Onboarding view | [`Shared/Views/Onboarding/OnboardingView.swift`](../../Shared/Views/Onboarding/OnboardingView.swift) | diff --git a/apps/ios/spec/impact.md b/apps/ios/spec/impact.md index 74acec789e..66444d8040 100644 --- a/apps/ios/spec/impact.md +++ b/apps/ios/spec/impact.md @@ -64,6 +64,7 @@ | Shared/Views/Chat/Group/GroupMemberInfoView.swift | PC3, PC14, PC16, PC30, PC31 | Medium | Member details and role management; rejected-by-operator status row for relay members | | Shared/Views/NewChat/NewChatView.swift | PC12, PC31 | High | New connection creation — onramp for all contacts and channels | | Shared/Views/NewChat/QRCode.swift | PC12 | Low | QR code display/scanning utility | +| Shared/Views/NewChat/WrongQRCode.swift | PC12, PC13, PC25, PC26, PC27 | Low | Wrong-QR-code alert message shared by all scanners | | Shared/Views/Call/ActiveCallView.swift | PC17 | Medium | Call UI rendering | | Shared/Views/Call/CallController.swift | PC17 | High | CallKit integration — call lifecycle | | Shared/Views/Call/WebRTCClient.swift | PC17 | High | WebRTC session management | 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 35d4d3fcde..8d38308d67 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -820,10 +820,10 @@ Wrong link This is a %s. To use it, open New chat, then scan or paste it there. This is a SimpleX server address. To use it, open Network & servers, Your servers, Add server, then Scan server QR code. - This is a link to migrate to another device. To use it, when setting up a new device, choose to migrate from another device. + This is a link to migrate to another device. To use it, choose Migrate when setting up a new device. This is an address to connect to a desktop app. To use it, open Use from desktop and scan the QR code shown in the desktop app. This is a security code. To use it, open the chat, then the contact\'s or member\'s name, then Verify security code. - This is a %s. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there. + This is a %s. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay. Permission Denied! @@ -3191,7 +3191,7 @@ Relay address - This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there. + This is a chat relay address, it cannot be used to connect. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay. Open channel Open new channel You are a subscriber