oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2
3#if os(iOS)
4 import UIKit
5#else
6 import AppKit
7#endif
8
9/// Call → Copy Invite Link (⌘⇧I): the meeting's invitation on the
10/// clipboard without opening the share sheet.
11///
12/// It asks the server the way the Share button does — `POST
13/// /m/{sqid}/share`, which hands a member the meeting's current sealed
14/// invitation, and mints one only if there is none. It never replaces or
15/// revokes a link (see store.shareLink and
16/// TestShareReusesSealedInvitationAndPreservesLegacyLinks).
17public enum InviteLinkCopy {
18 public enum Rule: Equatable, Sendable {
19 /// The menu item is off: there is no call to invite anyone to.
20 case unavailable
21 /// On, and a press copies the link.
22 case copy
23 /// On, and a press says why there is nothing to copy, rather than a
24 /// beep: a guest cannot share a meeting, and before the meeting has
25 /// said who you are, nobody knows yet.
26 case explain(String)
27 }
28
29 public static let shortcut = KeyboardShortcut("i", modifiers: [.command, .shift])
30 public static let title = "Copy Invite Link"
31
32 /// Whether ⌘⇧I does anything, and what.
33 public static func rule(inCall: Bool, phase: String?, standing: String) -> Rule {
34 guard inCall, phase != "ended" else { return .unavailable }
35 switch standing {
36 case "member": return .copy
37 case "": return .explain("The invite link isn’t ready yet. Try again in a moment.")
38 default: return .explain("Only members of this meeting can share its invite link.")
39 }
40 }
41}
42
43extension Call {
44 /// Copies the meeting's invite link, or says why not, in `notice` (the
45 /// brief message over the stage) and to VoiceOver.
46 public func copyInviteLink(inCall: Bool = true) async {
47 switch InviteLinkCopy.rule(inCall: inCall, phase: state?.phase, standing: standing) {
48 case .unavailable: return
49 case .explain(let why): say(why)
50 case .copy:
51 if invitationURL == nil { await loadInvitation() }
52 guard let url = invitationURL else {
53 say("Could not get the invite link. Try again.")
54 return
55 }
56 #if os(iOS)
57 UIPasteboard.general.url = url
58 #else
59 NSPasteboard.general.clearContents()
60 NSPasteboard.general.setString(url.absoluteString, forType: .string)
61 #endif
62 say("Invite link copied")
63 }
64 }
65
66 /// A short message over the call that goes by itself: never a dialog.
67 func say(_ text: String) {
68 toastSerial += 1
69 let serial = toastSerial
70 toast = text
71 AccessibilityNotification.Announcement(text).post()
72 Task { @MainActor [weak self] in
73 try? await Task.sleep(for: .seconds(2.5))
74 guard let self, self.toastSerial == serial else { return }
75 self.toast = nil
76 }
77 }
78}
79
80/// The call's brief message ("Invite link copied"), over the stage.
81/// Announced when it is set, so this is hidden from VoiceOver.
82public struct CallToast: View {
83 @ObservedObject private var call: Call
84 public init(call: Call) { self.call = call }
85 public var body: some View {
86 VStack {
87 if let text = call.toast {
88 Label(text, systemImage: text == "Invite link copied" ? "link" : "info.circle")
89 .font(.callout.weight(.medium))
90 .padding(.horizontal, 14).padding(.vertical, 8)
91 .background(.regularMaterial, in: Capsule())
92 .shadow(color: .black.opacity(0.2), radius: 8, y: 2)
93 .transition(.opacity)
94 .accessibilityHidden(true)
95 }
96 }
97 .animation(.easeInOut(duration: 0.15), value: call.toast)
98 .allowsHitTesting(false)
99 }
100}
101
102/// The Call menu's items, in order, as data the app builds its menu from
103/// and the tests can read: title and shortcut.
104public enum CallMenuItem: CaseIterable, Sendable {
105 case mute
106 case copyInviteLink
107
108 public func title(muted: Bool) -> String {
109 switch self {
110 case .mute: muted ? "Unmute Microphone" : "Mute Microphone"
111 case .copyInviteLink: InviteLinkCopy.title
112 }
113 }
114
115 public var shortcut: KeyboardShortcut {
116 switch self {
117 case .mute: KeyboardShortcut("m", modifiers: [.command, .shift])
118 case .copyInviteLink: InviteLinkCopy.shortcut
119 }
120 }
121}
122