Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2026-05-14 16:58:00 +01:00
6 changed files with 179 additions and 36 deletions
@@ -20,10 +20,11 @@ struct ChatItemForwardingView: View {
var composeState: Binding<ComposeState>? = nil
var isProhibited: ((Chat) -> Bool)? = nil
var onSelectChat: ((Chat) -> Void)? = nil
var includeLocal: Bool = true
@State private var searchText: String = ""
@State private var alert: SomeAlert?
private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats)
private var chatsToForwardTo: [Chat] { filterChatsToForwardTo(chats: ChatModel.shared.chats, includeLocal: includeLocal) }
var body: some View {
NavigationView {
@@ -1104,7 +1104,8 @@ func shareChannelPicker(groupInfo: GroupInfo, composeState: Binding<ComposeState
let v = ChatItemForwardingView(
title: "Share channel",
isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) },
onSelectChat: { chat in shareChatLink(chat, sourceGroupInfo: groupInfo, composeState: composeState) }
onSelectChat: { chat in shareChatLink(chat, sourceGroupInfo: groupInfo, composeState: composeState) },
includeLocal: false
)
if #available(iOS 16.0, *) {
v.presentationDetents([.fraction(0.8)])
+28 -28
View File
@@ -188,8 +188,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-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.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 */; };
@@ -571,8 +571,8 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
@@ -741,8 +741,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -828,8 +828,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.1.1-Fx4JRO2FuL8K4q8f3JAaMO.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.2.0-8CNxlktzYRPIWqOtKFvRIa.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -2083,7 +2083,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2108,7 +2108,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
@@ -2133,7 +2133,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2158,7 +2158,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
@@ -2175,11 +2175,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2195,11 +2195,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2220,7 +2220,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2235,7 +2235,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2257,7 +2257,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2272,7 +2272,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2294,7 +2294,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2320,7 +2320,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2345,7 +2345,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2372,7 +2372,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2399,7 +2399,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2414,7 +2414,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2433,7 +2433,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 331;
CURRENT_PROJECT_VERSION = 332;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2448,7 +2448,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.5.1;
MARKETING_VERSION = 6.5.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
+2 -2
View File
@@ -53,11 +53,11 @@ extension ChatLike {
}
}
public func filterChatsToForwardTo<C: ChatLike>(chats: [C]) -> [C] {
public func filterChatsToForwardTo<C: ChatLike>(chats: [C], includeLocal: Bool = true) -> [C] {
var filteredChats = chats.filter { c in
c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo)
}
if let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) {
if includeLocal, let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) {
filteredChats.insert(privateNotes, at: 0)
}
return filteredChats
+4 -4
View File
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.5.1
android.version_code=347
android.version_name=6.5.2
android.version_code=349
android.bundle=false
desktop.version_name=6.5.1
desktop.version_code=142
desktop.version_name=6.5.2
desktop.version_code=143
kotlin.version=2.1.20
gradle.plugin.version=8.7.0
@@ -0,0 +1,141 @@
# Share Channel Link — Filter Saved Messages (iOS)
Companion to [#6958](https://github.com/simplex-chat/simplex-chat/pull/6958) (`nd/fix-group-link-share`, Android/Desktop).
Branch `nd/fix-group-link-share-ios`, base `master`.
## 1. The bug
On the iOS channel-link "Share via chat" picker, **Saved Messages** is offered as a destination. Tapping it produces `chat commandError Failed reading: empty` from the server.
This is the iOS counterpart of bug #1 from PR #6958. Bug #2 from that PR (the "Share via chat" button rendering on plain groups) does **not** exist on iOS — `GroupLinkView.swift:110` already gates the button with `if groupInfo?.groupProfile.publicGroup != nil`, and plain groups have `publicGroup == nil`.
## 2. Root cause
`APIShareChatMsgContent` is parsed with `sendRefP` in `src/Simplex/Chat/Library/Commands.hs:5426`:
```haskell
sendRefP =
(A.char '@' $> SRDirect <*> A.decimal)
<|> (A.char '#' $> SRGroup <*> A.decimal <*> optional gcScopeP <*> asGroupP)
```
The iOS client emits `*<id>` for `ChatType.local` (Saved Messages) via the standard chat-type prefix. `sendRefP` has no `*` branch, attoparsec returns `Failed reading: empty`, the handler never runs.
This is the correct server behaviour — sharing a channel link to one's own note folder is not a meaningful operation. The picker offered the destination by accident: `filterChatsToForwardTo` in `apps/ios/SimpleXChat/ChatUtils.swift:56` unconditionally inserts `ChatInfo.local` at index 0:
```swift
public func filterChatsToForwardTo<C: ChatLike>(chats: [C]) -> [C] {
var filteredChats = chats.filter { c in
c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo)
}
if let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) {
filteredChats.insert(privateNotes, at: 0)
}
return filteredChats
}
```
`shareChannelPicker` (`apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift:1103`) builds a `ChatItemForwardingView`, which calls `filterChatsToForwardTo`. So the channel-link picker inherits the Saved-Messages-at-index-0 behaviour that the forward picker wants.
## 3. Approaches considered
| # | Approach | Note |
|---|----------|------|
| A | **Final** — parameterize the filter: add `includeLocal: Bool = true` to `filterChatsToForwardTo` and to `ChatItemForwardingView`; pass `includeLocal: false` from `shareChannelPicker`. | Default keeps existing call-sites untouched. Mirrors PR #6958's pattern — the filter decides, callers express intent. |
| B | Post-filter `.local` inside `ChatItemForwardingView` after the call to `filterChatsToForwardTo`. | Same line count, but duplicates the `.local` predicate at the consumer instead of expressing it at the producer. |
| C | Pass a closure filter to `ChatItemForwardingView`. | A closure encodes one bit as a function — strictly more machinery for the same outcome. |
| D | Mirror Kotlin literally: read a global `SharedContent.ChatLink` discriminator inside the filter. | iOS's `SharedContent` lives in the Share Extension target, not the main app — the Kotlin-style predicate doesn't translate. |
Approach A wins on minimality (5 lines, three files), preserves all default behaviour, and matches the architectural pattern of PR #6958 (decision lives where the data is produced).
## 4. Final implementation
### 4.1 `apps/ios/SimpleXChat/ChatUtils.swift` — add `includeLocal` parameter
```diff
-public func filterChatsToForwardTo<C: ChatLike>(chats: [C]) -> [C] {
+public func filterChatsToForwardTo<C: ChatLike>(chats: [C], includeLocal: Bool = true) -> [C] {
var filteredChats = chats.filter { c in
c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo)
}
- if let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) {
+ if includeLocal, let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) {
filteredChats.insert(privateNotes, at: 0)
}
return filteredChats
}
```
Default value preserves the contract for every existing caller (`ChatItemForwardingView.swift:26`, `SimpleX SE/ShareModel.swift:71-72`). The `if includeLocal, let ...` form Swift-natively short-circuits — no nested block needed.
### 4.2 `apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift` — thread the flag
```diff
var isProhibited: ((Chat) -> Bool)? = nil
var onSelectChat: ((Chat) -> Void)? = nil
+ var includeLocal: Bool = true
@State private var searchText: String = ""
@State private var alert: SomeAlert?
- private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats)
+ private var chatsToForwardTo: [Chat] { filterChatsToForwardTo(chats: ChatModel.shared.chats, includeLocal: includeLocal) }
```
`private let → private var` (computed) is required because Swift property initializers cannot read sibling instance properties. The computed form re-evaluates when `body` runs — in this view that is twice per render (lines 49 and 52), against a list of size `chats.count`. No meaningful cost; if a profile ever flagged it, switching to a custom `init(...)` that captures `includeLocal` once is a trivial follow-up.
### 4.3 `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift` — opt out from the channel-link picker
```diff
let v = ChatItemForwardingView(
title: "Share channel",
isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) },
- onSelectChat: { chat in shareChatLink(chat, sourceGroupInfo: groupInfo, composeState: composeState) }
+ onSelectChat: { chat in shareChatLink(chat, sourceGroupInfo: groupInfo, composeState: composeState) },
+ includeLocal: false
)
```
One-line opt-out from the only iOS site that uses the channel-link share flow.
### 4.4 What is *not* changed
- **`GroupLinkView.swift:110`** — already gates "Share via chat" with `groupInfo?.groupProfile.publicGroup != nil`. Bug #2 from PR #6958 has no iOS analog.
- **Forward picker** (`ChatView.swift:279, 282`) — uses `ChatItemForwardingView`'s default `includeLocal: true`. Saved Messages still appears at index 0.
- **Share extension** (`SimpleX SE/ShareModel.swift:71-72`) — calls `filterChatsToForwardTo` directly with the default. Unchanged.
- **Haskell.** `sendRefP` and `APIShareChatMsgContent` stay at master. The client just stops offering destinations the server refuses.
- **Android/Desktop, all other share/forward paths.**
## 5. Why this works
The server is the source of truth for which destinations are valid for `APIShareChatMsgContent`:
- Destinations: `@<id>` (direct), `#<id>` (group / scope). Local (`*<id>`) is rejected as a parse failure, by construction.
- Sources: groups with `publicGroup` and `groupLink`. iOS already gates the source side correctly.
The client's job is to offer choices the server will accept. The picker offered Local in error; this PR narrows the offer to match the server's grammar. The default-`true` parameter means every other caller keeps its current behaviour without modification.
## 6. Behaviour changes — full inventory
1. **Picking Saved Messages in the iOS share-channel-link picker is no longer possible.** This is the bug fix.
2. **Forward picker — unchanged.** Default `includeLocal: true`. Forward-to-Saved-Messages still works.
3. **Share extension picker — unchanged.** Default `includeLocal: true`.
4. **`GroupLinkView` button gate — unchanged.** Already correct on iOS.
Nothing else changes. Verified by reading the diff against master line-by-line.
## 7. Verification
1. **Diff is six insertions, four deletions across three files** (`git diff --stat`):
- `apps/ios/SimpleXChat/ChatUtils.swift | 4 ++--`
- `apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift | 3 ++-`
- `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift | 3 ++-`
2. **iOS build** requires Xcode on macOS — not run in this environment. To run by reviewer.
3. **Manual on iOS once built:**
- Open a public channel → profile → "Share via chat" → picker shows direct + group destinations only, **no "Saved Messages" row**.
- Long-press a message → Forward → picker still shows Saved Messages at the top (regression check).
- Open a plain group → group-link management → no "Share via chat" button (already correct, regression check).
## 8. Trade-offs and follow-ups
1. **Computed `chatsToForwardTo` re-evaluates on body refresh** rather than caching at struct init. In practice, twice per render against a small list, with `ChatModel.shared.chats` already SwiftUI-observed. Switching to a custom `init(...)` that captures `includeLocal` and assigns `chatsToForwardTo` once is a one-step refactor if ever needed.
2. **The flag is binary, not content-typed.** Kotlin discriminates on `SharedContent` variant; iOS uses an explicit caller intent. If a future iOS site needed to skip Local for a non-share-channel reason, the same flag applies — no further changes needed.