oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2import RastrilloMacUpdates
3#endif
4import AppKit
5@preconcurrency import CallsBridge
6@_spi(Fixtures) import CallsKit
7import SwiftUI
8import LiveKitWebRTC
9
10@main struct CallsMacApp: App {
11 #if os(macOS)
12 // Quiet: an update downloads in the background and installs when Calls quits,
13 // and nothing it finds opens a window. `isBusy` follows the call, so no check
14 // or download starts during one and the restart waits until it ends.
15 @State private var updater = NativeUpdater(policy: .quiet)
16 #endif
17 @NSApplicationDelegateAdaptor(CallsAppDelegate.self) private var appDelegate
18 @StateObject private var model = CallsModel()
19 var body: some Scene {
20 Window("Calls", id: "main") {
21 ContentView(model: model, updater: updater).frame(minWidth: 760, minHeight: 540).onDisappear { model.leave() }
22 .onOpenURL { model.openMeetingURL($0) }
23 .onAppear {
24 CallsAppDelegate.model = model
25 model.restore()
26 AudioWarmUp.afterLaunch()
27 }
28 .miniWindow(for: model)
29 .onChange(of: model.joining, initial: true) { updater.isBusy = model.joining }
30 }
31 .defaultSize(width: 1000, height: 760)
32 .commands {
33 CommandGroup(replacing: .newItem) {}
34 NativeUpdateCommands(updater: updater)
35 CommandMenu("Call") { CallMenuItems(model: model) }
36 }
37 Settings {
38 CallsSettingsView(
39 model: model,
40 automaticUpdates: updater.canInstallUpdatesAutomatically
41 ? Binding(get: { updater.automaticallyInstallsUpdates }, set: { updater.automaticallyInstallsUpdates = $0 })
42 : nil)
43 }
44 }
45}
46
47final class CallsAppDelegate: NSObject, NSApplicationDelegate {
48 /// Set by the app so Apple's answer can reach the session that asked.
49 /// Weak-ish by convention rather than by keyword: the model outlives
50 /// the delegate for the whole life of the process.
51 @MainActor static var model: CallsModel?
52
53 func application(
54 _ application: NSApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data
55 ) {
56 Task { @MainActor in CallsAppDelegate.model?.registerDevice(token: token) }
57 }
58
59 func application(
60 _ application: NSApplication, didFailToRegisterForRemoteNotificationsWithError error: Error
61 ) {
62 // Expected on any build without the push entitlement, which is every
63 // ad-hoc signed local build. Silent: the person did not ask for this
64 // and the consequence is simply no push.
65 }
66
67 /// Quitting mid-call leaves the call first. Without this the process
68 /// ended with its participation still in the room, inside the server's
69 /// thirty-second resume window, and the next launch — an update's
70 /// relaunch, most often — joined as a second participation of the same
71 /// session and was told "This meeting is open elsewhere". (The core now
72 /// takes its own place back in that case too; leaving is still the
73 /// right thing to do, because the others in the call see you go at
74 /// once rather than thirty seconds later.) The leave is given three
75 /// seconds; the core's own request gives up after two.
76 func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
77 guard let model = CallsAppDelegate.model, model.joining else { return .terminateNow }
78 var replied = false
79 let reply = { @MainActor in
80 guard !replied else { return }
81 replied = true
82 sender.reply(toApplicationShouldTerminate: true)
83 }
84 Task { @MainActor in
85 await model.leaveNow()
86 reply()
87 }
88 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { MainActor.assumeIsolated { reply() } }
89 return .terminateLater
90 }
91
92 func applicationDidFinishLaunching(_ notification: Notification) {
93 // On macOS 26 and later a bundled app's Dock icon comes from Assets.car,
94 // which the system masks and renders in the person's icon style. Setting
95 // a picture here would replace that with a flat PNG, so only a run
96 // without the bundle's icon (swift run) or an older macOS sets one.
97 if #available(macOS 26, *), Bundle.main.object(forInfoDictionaryKey: "CFBundleIconName") != nil {
98 return
99 }
100 if let url = Bundle.main.url(forResource: "MacIcon", withExtension: "png")
101 ?? Bundle.module.url(forResource: "MacIcon", withExtension: "png")
102 {
103 NSApp.applicationIconImage = NSImage(contentsOf: url)
104 }
105 }
106}
107
108/// The Call menu. Mute is here so that its shortcut can be found, and so
109/// that there is one when Space belongs to a text field or a button. Copy
110/// Invite Link puts the meeting's link on the clipboard without the share
111/// sheet. Both work from the mini window too: the menu is the app's.
112struct CallMenuItems: View {
113 @ObservedObject var model: CallsModel
114 var body: some View { CallMenuButtons(call: model.call, inCall: model.joining) }
115}
116
117private struct CallMenuButtons: View {
118 @ObservedObject var call: Call
119 let inCall: Bool
120 var body: some View {
121 ForEach(CallMenuItem.allCases, id: \.self) { item in
122 switch item {
123 case .mute:
124 Button(item.title(muted: call.muted)) { call.mute() }
125 .keyboardShortcut(item.shortcut)
126 .disabled(!inCall)
127 case .copyInviteLink:
128 Button(item.title(muted: call.muted)) {
129 Task { await call.copyInviteLink(inCall: inCall) }
130 }
131 .keyboardShortcut(item.shortcut)
132 .disabled(
133 InviteLinkCopy.rule(inCall: inCall, phase: call.state?.phase, standing: call.standing)
134 == .unavailable)
135 }
136 }
137 Divider()
138 // Spec §15.26: during a call, and for ten minutes after it.
139 Button("Report a Problem\u{2026}") { call.reportProblem() }
140 .disabled(!call.canReportProblem)
141 }
142}
143
144struct ContentView: View {
145 @ObservedObject var model: CallsModel
146 let updater: NativeUpdater
147 var body: some View {
148 Group {
149 if let fixture = MacCallFixtureView.requested {
150 MacCallFixtureView(fixture: fixture)
151 } else if model.joining {
152 // The layout pads itself: the classic view sits in a margin, the
153 // full-window one goes to the window's edges.
154 MacCallView(call: model.call, leave: model.leave, rejoin: model.join)
155 } else {
156 // The home screen is the only place an update is mentioned: never the call.
157 CallsLobby(model: model).safeAreaInset(edge: .bottom) {
158 if updater.canInstallPendingUpdate { NativeUpdateNotice(updater: updater).padding(.bottom, 12) }
159 }
160 }
161 }.tint(CallsAppearance.accent)
162 }
163}
164
165/// The full-window call with no server behind it, in the real window, to
166/// see the title bar, the traffic lights and the drag strip for real.
167/// Debug builds only: `-CallsCallFixture focus` (or grid, screen,
168/// notices), optionally `-CallsFixtureBackdrop black|white|busy`,
169/// `-CallsFixtureSelf muted|cameraOff|sharing` and `-CallsFixtureControls
170/// shown|hidden`; without that last, the pointer decides as in a call.
171struct MacCallFixtureView: View {
172 static var requested: CallFixture? {
173 #if DEBUG
174 UserDefaults.standard.string(forKey: "CallsCallFixture").flatMap(CallFixture.init(rawValue:))
175 #else
176 nil
177 #endif
178 }
179
180 let fixture: CallFixture
181 @StateObject private var call = Call(playAudio: false)
182 /// `-CallsFixtureJoinAfter <s>` starts on a stand-in for the lobby and
183 /// joins after that long; `-CallsFixtureLeaveAfter <s>` goes back to it:
184 /// the window's title bar has to come and go with the call.
185 @State private var inCall = UserDefaults.standard.object(forKey: "CallsFixtureJoinAfter") == nil
186
187 var body: some View {
188 Group {
189 if inCall {
190 callView
191 } else {
192 Text("Lobby stand-in").frame(maxWidth: .infinity, maxHeight: .infinity)
193 }
194 }
195 .task {
196 let defaults = UserDefaults.standard
197 if defaults.object(forKey: "CallsFixtureJoinAfter") != nil {
198 try? await Task.sleep(for: .seconds(defaults.double(forKey: "CallsFixtureJoinAfter")))
199 inCall = true
200 }
201 if defaults.object(forKey: "CallsFixtureLeaveAfter") != nil {
202 try? await Task.sleep(for: .seconds(defaults.double(forKey: "CallsFixtureLeaveAfter")))
203 inCall = false
204 }
205 }
206 }
207
208 private var callView: some View {
209 let defaults = UserDefaults.standard
210 return MacCallView(
211 call: call, grid: fixture == .grid,
212 controlsShown: defaults.string(forKey: "CallsFixtureControls").map { $0 == "shown" },
213 leave: {}, rejoin: {}
214 )
215 .environment(
216 \.fixtureBackdrop,
217 defaults.string(forKey: "CallsFixtureBackdrop").flatMap(FixtureBackdrop.init(rawValue:)))
218 .onAppear {
219 fixture.apply(to: call)
220 defaults.string(forKey: "CallsFixtureSelf").flatMap(FixtureSelf.init(rawValue:))?
221 .apply(to: call)
222 }
223 }
224}
225
226