eleven / messenger Public

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>
Dermot Daly c937e2f549b85b419044536bff03ff258fe711f5 parent b86f81f
3 files changed, +38 −21
  • ios/LChatCore/Sources/LChatCore/MessageStore.swift +27 −13
  • ios/LChatMac/LChatMac-Dev.entitlements +2 −1
  • ios/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>