mac: give the app its App Group so message search works (#1121) (#1214)
Search on Mac returned nothing while the message was visible in the feed. iOS PR #423 moved MessageStore's local SQLite/FTS index into the `group.com.elevenmessenger.messenger` App Group container (so the NSE could share it), but the change was iOS-only: the sandboxed Mac app was never granted the matching entitlement. Without it the store can't open, so upsert/indexText/search all no-op — while the feed still renders from the network-backed in-memory model, hence "visible but unsearchable". - Add com.apple.security.application-groups to both LChatMac.entitlements and LChatMac-Dev.entitlements, mirroring iOS. - Harden MessageStore.storeDir(): confirm the group dir actually exists after createDirectory (a sandboxed app lacking the entitlement can get a non-nil URL it still can't write), and ensure the Application Support fallback exists before returning it — so a signing/entitlement gap degrades gracefully instead of silently breaking search. Requires the App Group capability to be enabled on the Mac App ID in the developer portal for archive/ship signing.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
3 files changed,
+38
−21
ios/LChatCore/Sources/LChatCore/MessageStore.swift+27 −13ios/LChatMac/LChatMac-Dev.entitlements+2 −1ios/LChatMac/LChatMac.entitlements+9 −7
diff --git a/ios/LChatCore/Sources/LChatCore/MessageStore.swift b/ios/LChatCore/Sources/LChatCore/MessageStore.swift| index 4266692..973bc71 100644 |
| --- a/ios/LChatCore/Sources/LChatCore/MessageStore.swift |
| +++ b/ios/LChatCore/Sources/LChatCore/MessageStore.swift |
| @@ -49,21 +49,35 @@ public actor MessageStore { |
| let fallback = (fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first |
| ?? URL(fileURLWithPath: NSTemporaryDirectory())) |
| .appendingPathComponent("lchat", isDirectory: true) |
| - guard let group = fm.containerURL(forSecurityApplicationGroupIdentifier: Self.appGroupID) |
| - else { return fallback } |
| - let dir = group.appendingPathComponent("lchat", isDirectory: true) |
| - try? fm.createDirectory(at: dir, withIntermediateDirectories: true) |
| - // One-time migration from the pre-group location: move the db (and its |
| - // WAL sidecars) so existing history survives the path change. |
| - let old = fallback.appendingPathComponent("messages.db") |
| - let new = dir.appendingPathComponent("messages.db") |
| - if fm.fileExists(atPath: old.path), !fm.fileExists(atPath: new.path) { |
| - for suffix in ["", "-wal", "-shm"] { |
| - try? fm.moveItem(at: URL(fileURLWithPath: old.path + suffix), |
| - to: URL(fileURLWithPath: new.path + suffix)) |
| + // Prefer the shared group container. On a sandboxed app that lacks the |
| + // App Group entitlement, `containerURL` can still hand back a path the |
| + // sandbox then refuses to create — so we don't trust the URL alone, |
| + // we confirm the directory actually exists after trying to create it. |
| + // If it doesn't, fall back to Application Support rather than let every |
| + // store op fail on a dead handle (which reads as "search finds nothing" |
| + // while the network-backed feed still renders — #1121). |
| + if let group = fm.containerURL(forSecurityApplicationGroupIdentifier: Self.appGroupID) { |
| + let dir = group.appendingPathComponent("lchat", isDirectory: true) |
| + try? fm.createDirectory(at: dir, withIntermediateDirectories: true) |
| + if fm.fileExists(atPath: dir.path) { |
| + // One-time migration from the pre-group location: move the db |
| + // (and its WAL sidecars) so existing history survives the path |
| + // change. |
| + let old = fallback.appendingPathComponent("messages.db") |
| + let new = dir.appendingPathComponent("messages.db") |
| + if fm.fileExists(atPath: old.path), !fm.fileExists(atPath: new.path) { |
| + for suffix in ["", "-wal", "-shm"] { |
| + try? fm.moveItem(at: URL(fileURLWithPath: old.path + suffix), |
| + to: URL(fileURLWithPath: new.path + suffix)) |
| + } |
| + } |
| + return dir |
| } |
| } |
| - return dir |
| + // No usable group container — make sure the fallback exists before we |
| + // hand it back, so open() can create the db there. |
| + try? fm.createDirectory(at: fallback, withIntermediateDirectories: true) |
| + return fallback |
| } |
| private func open() -> OpaquePointer? { |
diff --git a/ios/LChatMac/LChatMac-Dev.entitlements b/ios/LChatMac/LChatMac-Dev.entitlements| index d0eb2e6..eaa17d2 100644 |
| --- a/ios/LChatMac/LChatMac-Dev.entitlements |
| +++ b/ios/LChatMac/LChatMac-Dev.entitlements |
| @@ -30,7 +30,8 @@ |
| <string>$(AppIdentifierPrefix)com.elevenmessenger.messenger.shared</string> |
| </array> |
| <!-- App group — see LChatMac.entitlements: without it the sandbox denies |
| - the avatar cache's group container and banners monogram forever. --> |
| + both the avatar cache (banners monogram forever) and MessageStore's |
| + local search index (search returns nothing, #1121). --> |
| <key>com.apple.security.application-groups</key> |
| <array> |
| <string>group.com.elevenmessenger.messenger</string> |
diff --git a/ios/LChatMac/LChatMac.entitlements b/ios/LChatMac/LChatMac.entitlements| index 8a1311d..59fe79c 100644 |
| --- a/ios/LChatMac/LChatMac.entitlements |
| +++ b/ios/LChatMac/LChatMac.entitlements |
| @@ -23,13 +23,15 @@ |
| <array> |
| <string>$(AppIdentifierPrefix)com.elevenmessenger.messenger.shared</string> |
| </array> |
| - <!-- Same app group as iOS: SharedAvatarCache resolves its directory to the |
| - group container (containerURL returns the path on macOS whether or not |
| - the entitlement exists), so a sandboxed build WITHOUT this entitlement |
| - has every avatar read/write silently denied — banners fall back to the |
| - monogram forever. Missing since sandboxing landed (#474). The Mac App |
| - ID needs the App Groups capability so the embedded profile authorizes |
| - the group (unprovisioned group access prompts on macOS 15+). --> |
| + <!-- Same app group as iOS. Two on-device caches resolve into this container |
| + (containerURL returns the path on macOS whether or not the entitlement |
| + exists, so a sandboxed build WITHOUT it has all access silently denied): |
| + SharedAvatarCache — every avatar read/write is denied, banners fall back |
| + to the monogram forever (#474); and MessageStore's local SQLite/FTS index |
| + (LChatCore/MessageStore.swift, storeDir) — the store can't open, so |
| + message search returns nothing while the feed still renders (#1121). The |
| + Mac App ID needs the App Groups capability so the embedded profile |
| + authorizes the group (unprovisioned group access prompts on macOS 15+). --> |
| <key>com.apple.security.application-groups</key> |
| <array> |
| <string>group.com.elevenmessenger.messenger</string> |