oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import UserNotifications
3
4#if os(iOS)
5 import UIKit
6#else
7 import AppKit
8#endif
9
10/// Notices when someone new is waiting to be let in, and says so once.
11///
12/// The admission queue is already on screen for a member who is looking at
13/// the call. This is for the member who is not: another Space, another
14/// app, a phone in a pocket. Without it, someone knocking on a call whose
15/// host has tabbed away waits until the host happens to look.
16///
17/// The decision of *whether* to announce is kept apart from the act of
18/// announcing, because the decision is the part with rules in it — once
19/// per person, never for someone already announced, forgotten when they
20/// stop waiting so a re-knock is heard again — and the act needs a
21/// notification centre and a real bundle.
22@MainActor public final class KnockWatch {
23 /// Peers already announced and still waiting. Kept rather than a plain
24 /// count so that one person leaving the queue and another arriving is
25 /// one announcement and not none.
26 private(set) var announced: Set<String> = []
27
28 public init() {}
29
30 /// Returns the people worth announcing, and records them.
31 ///
32 /// `visible` is the whole of the "don't shout at someone already
33 /// looking" rule: when the queue is on screen, arrivals are recorded as
34 /// though announced, so tabbing away afterwards does not then produce a
35 /// pile of notifications about people who have been waiting for a
36 /// while. Only someone who arrives *while* you are away is news.
37 public func arrivals(waiting: [RosterPeer], visible: Bool) -> [RosterPeer] {
38 let present = Set(waiting.map(\.peer))
39 // Someone who left the queue is forgotten, so that knocking again
40 // after being let in — or after giving up — is heard again.
41 announced.formIntersection(present)
42 let fresh = waiting.filter { !announced.contains($0.peer) }
43 announced.formUnion(present)
44 return visible ? [] : fresh
45 }
46
47 /// Dropped when a call ends: a queue from a finished meeting has
48 /// nothing to say about the next one.
49 public func reset() { announced = [] }
50}
51
52/// The act, as opposed to the decision. Local notifications only: there
53/// is no APNs anywhere in this suite — no push key, no device tokens, no
54/// sender — so this reaches a member whose app is running and not
55/// frontmost, and not one whose app is closed. That second case is real
56/// and is not solved here.
57@MainActor public enum KnockNotice {
58 /// True when the app is in front, and the queue therefore already
59 /// visible to anyone looking at it.
60 public static var appIsFrontmost: Bool {
61 #if os(iOS)
62 return UIApplication.shared.applicationState == .active
63 #else
64 return NSApp?.isActive ?? false
65 #endif
66 }
67
68 /// Asks once, at join, rather than at the first knock: a permission
69 /// sheet arriving at the same moment as the thing it is about is a
70 /// sheet that gets dismissed to get at the thing.
71 public static func requestPermission() {
72 guard let centre = centre else { return }
73 centre.requestAuthorization(options: [.alert, .sound]) { _, _ in }
74 }
75
76 public static func announce(_ people: [RosterPeer]) {
77 guard let centre = centre, !people.isEmpty else { return }
78 for person in people {
79 let content = UNMutableNotificationContent()
80 content.title = "Someone is waiting to join"
81 // The person's own name, which came from the server's roster and is
82 // someone else's text — so it goes in the body as data and never
83 // into a format string that could be read as anything else.
84 content.body = person.name
85 content.sound = .default
86 centre.add(
87 UNNotificationRequest(
88 identifier: "knock-" + person.peer, content: content, trigger: nil))
89 }
90 }
91
92 /// Nil outside a real application bundle. UNUserNotificationCenter
93 /// raises rather than returning an error when there is no bundle
94 /// identifier to register against, which is every unit test and the
95 /// interop executable — neither of which should take the app down for
96 /// want of a notification nobody would see.
97 private static var centre: UNUserNotificationCenter? {
98 guard Bundle.main.bundleIdentifier != nil else { return nil }
99 return UNUserNotificationCenter.current()
100 }
101}
102