eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2
3// Animated-GIF playback for the native Mac feed. BlobImageLoader decodes an
4// attachment's decrypted bytes with NSImage(data:), which keeps every GIF
5// frame in its bitmap rep — so the cached image animates as-is inside an
6// NSImageView (`animates = true`), no second decode and no second cache.
7// SwiftUI's Image would flatten it to the first frame, which is exactly what
8// the paused state shows (matching the web's canvas-drawn still).
9
10/// Whether the system asks for reduced motion — animated GIFs then start
11/// paused (a click still plays them). The web twin is chat.js honouring
12/// prefers-reduced-motion in wireGifToggle.
13func gifsStartPaused() -> Bool {
14 #if canImport(AppKit) && !targetEnvironment(macCatalyst)
15 return NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
16 #else
17 // iOS renders GIFs as stills in the native beta feed (the default feed is
18 // the web page, which handles reduced motion itself), so nothing to gate.
19 return false
20 #endif
21}
22
23#if canImport(AppKit) && !targetEnvironment(macCatalyst)
24import AppKit
25
26extension NSImage {
27 /// Frames in the image's bitmap rep — >1 means it will actually animate.
28 /// A still that merely claims image/gif (single-frame GIF) reports 1 and
29 /// keeps the ordinary photo behaviour (lightbox on click).
30 var gifFrameCount: Int {
31 guard let rep = representations.first as? NSBitmapImageRep,
32 let n = rep.value(forProperty: .frameCount) as? Int else { return 1 }
33 return n
34 }
35}
36
37/// NSImageView host for an animated GIF; `playing` pauses/resumes (paused
38/// shows the first frame). Events pass straight through the AppKit view, so
39/// the SwiftUI container keeps the tap toggle, the right-click menu, and
40/// drag-out exactly as for still photos.
41struct AnimatedGIFView: NSViewRepresentable {
42 let image: NSImage
43 let playing: Bool
44
45 func makeNSView(context: Context) -> ClickThroughImageView {
46 let v = ClickThroughImageView()
47 v.imageScaling = .scaleProportionallyUpOrDown
48 v.image = image
49 v.animates = playing
50 // The SwiftUI .frame decides the size — the GIF's intrinsic size must
51 // not push the bubble wider than the reserved w/h box.
52 v.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
53 v.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
54 return v
55 }
56
57 func updateNSView(_ v: ClickThroughImageView, context: Context) {
58 if v.image !== image { v.image = image }
59 v.animates = playing
60 }
61
62 // NSImageView is an NSControl, so left it would swallow the clicks the
63 // bubble needs; returning nil routes every event to the SwiftUI layer.
64 final class ClickThroughImageView: NSImageView {
65 override func hitTest(_ point: NSPoint) -> NSView? { nil }
66 }
67}
68#endif
69