mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 17:58:47 +00:00
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.
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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<ScanResult, ScanError>) {
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -820,10 +820,10 @@
|
||||
<string name="wrong_link">Wrong link</string>
|
||||
<string name="wrong_qr_connection_link">This is a %s. To use it, open New chat, then scan or paste it there.</string>
|
||||
<string name="wrong_qr_server_address">This is a SimpleX server address. To use it, open Network & servers, Your servers, Add server, then Scan server QR code.</string>
|
||||
<string name="wrong_qr_migration_link">This is a link to migrate to another device. To use it, when setting up a new device, choose to migrate from another device.</string>
|
||||
<string name="wrong_qr_migration_link">This is a link to migrate to another device. To use it, choose Migrate when setting up a new device.</string>
|
||||
<string name="wrong_qr_desktop_address">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.</string>
|
||||
<string name="wrong_qr_security_code">This is a security code. To use it, open the chat, then the contact\'s or member\'s name, then Verify security code.</string>
|
||||
<string name="wrong_qr_relay_address">This is a %s. To use it, open Network & servers, Your servers, Add server, then Chat relay, and paste the address there.</string>
|
||||
<string name="wrong_qr_relay_address">This is a %s. To use it, open Network & servers, Your servers, Add server, then Chat relay, paste the address, then Test relay.</string>
|
||||
|
||||
<!-- GetImageView -->
|
||||
<string name="toast_permission_denied">Permission Denied!</string>
|
||||
@@ -3191,7 +3191,7 @@
|
||||
|
||||
<!-- ConnectPlan.kt channel-related -->
|
||||
<string name="relay_address_alert_title">Relay address</string>
|
||||
<string name="relay_address_alert_message">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.</string>
|
||||
<string name="relay_address_alert_message">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.</string>
|
||||
<string name="connect_plan_open_channel">Open channel</string>
|
||||
<string name="connect_plan_open_new_channel">Open new channel</string>
|
||||
<string name="connect_plan_you_are_subscriber">You are a subscriber</string>
|
||||
|
||||
Reference in New Issue
Block a user