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/// "Join a call as a guest": paste the invite link you were sent, and
4/// that meeting's own browser sign-in runs for it, on whatever server the
5/// link names. Nothing about it is kept.
6///
7/// The clipboard is read only through the Paste control, so iPhone never
8/// asks whether Calls may paste, and nothing is read that the person did
9/// not hand over.
10@MainActor public struct GuestJoinView: View {
11 @ObservedObject private var model: CallsModel
12 private let done: () -> Void
13 @State private var link: String
14 @State private var problem: String = ""
15
16 /// `problem` shows the link's validation at once — for snapshots of
17 /// the inline error.
18 public init(
19 model: CallsModel, link: String = "", problem: Bool = false, done: @escaping () -> Void
20 ) {
21 self.model = model
22 self.done = done
23 _link = State(initialValue: link)
24 var message = ""
25 if problem, case .failure(let failure) = MeetingLink.validate(link) {
26 message = failure.message
27 }
28 _problem = State(initialValue: message)
29 }
30
31 public var body: some View {
32 NavigationStack {
33 Form {
34 Section {
35 TextField("Invite link", text: $link, prompt: Text("https://…/j/…"))
36 .accessibilityIdentifier("inviteLink")
37 #if os(iOS)
38 .keyboardType(.URL).textInputAutocapitalization(.never).autocorrectionDisabled()
39 #endif
40 .onSubmit(join)
41 .onChange(of: link) { problem = "" }
42 PasteButton(payloadType: String.self) { strings in
43 guard let text = strings.first else { return }
44 Task { @MainActor in paste(text) }
45 }
46 .accessibilityIdentifier("pasteInvite")
47 if !problem.isEmpty {
48 Label(problem, systemImage: "exclamationmark.circle.fill")
49 .foregroundStyle(.red).fixedSize(horizontal: false, vertical: true)
50 .accessibilityLabel("Error: \(problem)")
51 }
52 } footer: {
53 Text(
54 "Paste the link someone sent you. Your browser opens to confirm who you are for this meeting; nothing is saved on this device, and your Calls server stays \(model.serverHost)."
55 )
56 }
57 }
58 .formStyle(.grouped)
59 #if os(iOS)
60 .navigationTitle("Join as a guest")
61 .navigationBarTitleDisplayMode(.inline)
62 .toolbar {
63 ToolbarItem(placement: .cancellationAction) { Button("Cancel", action: done) }
64 ToolbarItem(placement: .confirmationAction) { joinButton }
65 }
66 #else
67 // A Mac sheet has no title bar to carry these, so they sit under
68 // the form where a Mac dialog keeps its buttons.
69 .safeAreaInset(edge: .top) {
70 Text("Join as a guest").font(.title2.bold())
71 .frame(maxWidth: .infinity, alignment: .leading).padding([.horizontal, .top], 20)
72 }
73 .safeAreaInset(edge: .bottom) {
74 HStack {
75 Spacer()
76 Button("Cancel", action: done).keyboardShortcut(.cancelAction)
77 joinButton.keyboardShortcut(.defaultAction)
78 }.padding([.horizontal, .bottom], 20)
79 }
80 #endif
81 }
82 #if os(macOS)
83 .frame(width: 460)
84 .fixedSize(horizontal: false, vertical: true)
85 #endif
86 .tint(CallsAppearance.accent)
87 }
88
89 private var joinButton: some View {
90 Button("Join", action: join).accessibilityIdentifier("guestJoin")
91 .disabled(link.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
92 }
93
94 /// Fills the field only with a Calls link; anything else on the
95 /// clipboard is left there, and said so.
96 private func paste(_ text: String) {
97 if case .success(let url) = MeetingLink.validate(text) {
98 link = url.absoluteString
99 problem = ""
100 } else {
101 problem = "The clipboard doesn’t hold a Calls invite link."
102 }
103 }
104
105 private func join() {
106 if let failure = model.joinAsGuest(link) {
107 problem = failure.message
108 } else {
109 done()
110 }
111 }
112}
113