eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2#if canImport(UIKit)
3import UIKit
4#else
5import AppKit
6#endif
7
8// πŸ€– AppIdentity β€” the native half of the icon system (docs/icons.md): which
9// mark an app wears in natively-rendered rows, headers, and pickers. Three
10// kinds, mirroring the web exactly:
11//
12// 1. First-party apps β†’ an SF Symbol.
13// 2. Integration apps β†’ the real brand mark, vendored as a monochrome
14// template PNG in Marks/ (brands have no SF Symbols; these are the same
15// SVGs web/static/logos.js carries, rasterized black-on-transparent so
16// the platform tints them like a symbol).
17// 3. Everything else β†’ emoji stays CONTENT, never chrome: a member-made
18// app or bot keeps its chosen glyph (the avatar call sites all pass it).
19//
20// WHICH symbol or mark an app wears is not native knowledge anymore: each
21// app declares it in its own JS spec beside its web `logo` (`symbol:` /
22// `mark:`, carried by the manifest report β€” docs/native-apps.md). AppRegistry
23// pushes the decoration snapshot here on every catalogue load, the bundled
24// seed included, so nothing hand-kept can drift; this file only RENDERS
25// (name β†’ SF Symbol string, asset key β†’ vendored template image).
26public enum AppIdentity {
27 private static let lock = NSLock()
28 nonisolated(unsafe) private static var symbols: [String: String] = [:]
29 nonisolated(unsafe) private static var markKeys: [String: String] = [:]
30
31 /// The registry's feed (seed at init, live reports after): app id β†’
32 /// declared SF Symbol / mark asset. Merged, new values winning β€” several
33 /// spaces' registries share this one decoration surface.
34 static func updateDecoration(symbols s: [String: String], marks m: [String: String]) {
35 lock.lock()
36 defer { lock.unlock() }
37 symbols.merge(s) { _, new in new }
38 markKeys.merge(m) { _, new in new }
39 }
40
41 /// The SF Symbol an app-thread's avatar renders, or nil when the app
42 /// wears a brand mark (see mark(for:)) or declared none. No generic
43 /// fallback: an app with no declared chrome renders its manifest glyph
44 /// (the avatar call sites all pass it), so a member app wears its own
45 /// emoji, never a generic grid. Call sites that NEED a symbol (iOS menus
46 /// drop non-image Label icons) keep their own ?? "square.grid.2x2".
47 public static func symbol(for appId: String?) -> String? {
48 guard let id = appId else { return nil }
49 lock.lock()
50 let name = symbols[id]
51 lock.unlock()
52 // Member apps can declare a symbol too (their manifest, like a
53 // built-in's) β€” but a name this OS can't resolve must fall back to
54 // the glyph, never render a blank. Resolution is cached; the rows
55 // re-render constantly.
56 guard let name else { return nil }
57 return resolvable(name) ? name : nil
58 }
59
60 private static func resolvable(_ name: String) -> Bool {
61 lock.lock()
62 if let known = resolvableCache[name] {
63 lock.unlock()
64 return known
65 }
66 lock.unlock()
67 #if canImport(UIKit)
68 let ok = UIImage(systemName: name) != nil
69 #else
70 let ok = NSImage(systemSymbolName: name, accessibilityDescription: nil) != nil
71 #endif
72 lock.lock()
73 resolvableCache[name] = ok
74 lock.unlock()
75 return ok
76 }
77
78 nonisolated(unsafe) private static var resolvableCache: [String: Bool] = [:]
79
80 /// The vendored brand mark for an integration app, template-tinted by the
81 /// caller. The app names its asset (manifest `mark`); only assets that
82 /// actually ship in Marks/ render β€” a custom app naming one it wishes it
83 /// had simply falls back to its glyph. Cached β€” the PNGs are tiny and the
84 /// rows re-render constantly.
85 public static func mark(for appId: String?) -> PlatformImage? {
86 guard let id = appId else { return nil }
87 lock.lock()
88 let file = markKeys[id]
89 lock.unlock()
90 guard let file else { return nil }
91 if let cached = cache[id] { return cached }
92 guard let url = Bundle.module.url(forResource: file, withExtension: "png", subdirectory: "Marks"),
93 let data = try? Data(contentsOf: url),
94 let image = PlatformImage(data: data) else { return nil }
95 cache[id] = image
96 return image
97 }
98
99 private nonisolated(unsafe) static var cache: [String: PlatformImage] = [:]
100}
101