oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1{{define "head"}}<link rel="stylesheet" href="{{asset "static/calls.css"}}">
2<noscript><link rel="stylesheet" href="{{asset "static/noscript.css"}}"></noscript>
3<link rel="stylesheet" href="{{asset "static/chat.css"}}">
4<script defer src="{{asset "static/chat.js"}}"></script>
5<script defer src="{{asset "static/report.js"}}"></script>
6<script defer src="{{asset "static/room.js"}}"></script>
7<script defer src="{{asset "static/call-layout.js"}}"></script>
8<script defer src="{{asset "static/call-window.js"}}"></script>
9{{end}}
10{{/* The call window (2026-09-28). Paul: "Can the web UI for a call get
11 an update so it more closely matches the Mac experience, with the
12 whole call UI in a rounded rect 'window' with a bit of padding
13 within the viewport?" So the room replaces the page shell: one
14 rounded window, inset from the viewport's edges (edge to edge on a
15 phone), with the picture filling it and everything else over the
16 picture, as the Mac's full-window call has it (MacFullWindowCall).
17
18 No title pill (2026-10-02, Paul approved): how the call is going is
19 said by the notices, and a pill saying it again was clutter. A title
20 someone chose is shown the way a Mac window shows its title — plain,
21 centred in the strip across the top, over a dark scrim, fading with
22 the controls. A title Calls wrote ("Call, 2 Oct 15:40") is not shown
23 at all; it stays the page's heading for a screen reader and its
24 <title>. What the pill's ☰ menu carried — who you are, Meetings,
25 Settings, Help, Sign out — is in the bar's ⋯ More menu.
26
27 With no script the header is a plain block instead (noscript.css):
28 the title whichever kind it is, the standing line, the floor notice
29 and the page's navigation, all server-rendered (AGENTS.md). */}}
30{{define "body"}}
31<main id="main" class="calls-window" aria-labelledby="call-title">
32<header id="call-top" class="calls-top calls-fades{{if .Named}} calls-top-named{{end}}">
33 <h1 id="call-title">{{.Title}}</h1>
34 <noscript>
35 <p class="calls-top-line">You’re in this call as {{.Name}}{{if eq .Standing "guest"}}, a guest{{end}}.{{if .Starts}} Starts {{.Starts}}.{{end}}</p>
36 {{if .Floor}}<p class="calls-top-line calls-floor">Calls outside your network may not connect yet.</p>{{end}}
37 <nav class="calls-top-nav" aria-label="Calls">
38 {{with .Menu.Identity}}<p class="calls-top-line">Signed in as {{.}}</p>{{end}}
39 <p class="calls-top-links">{{range .Menu.Nav}}<a href="{{.Href}}">{{.Label}}</a> {{end}}{{range .Menu.Help}}<a href="{{.Href}}">{{.Label}}</a> {{end}}{{with .Menu.SignOut}}<form method="post" action="{{.Action}}"><button type="submit">{{.Label}}</button></form>{{end}}</p>
40 </nav>
41 </noscript>
42</header>
43
44{{/* The Share form appears twice on purpose, and never at the same
45 time. The bar's copy below is inside #room, which room.js un-hides,
46 so it is the scripted path and matches the native call bar. This
47 copy is the scriptless one: <noscript> is parsed as markup only when
48 scripting is off, so exactly one of the two is ever real. Without it
49 the sentence below is false, which is how it stood between c80a506
50 and a6c2ac8. "End meeting for everyone" is here for the same reason: with
51 script it is in the bar's More menu, inside #room. */}}
52<noscript><div class="calls-scriptless">
53 <p>The call needs JavaScript to reach your camera and the other participants; everything else on this page works without it. <a href="/m/{{.Sqid}}/join">Back to the holding screen</a>.</p>
54 {{if .IsMember}}<form method="post" action="/m/{{.Sqid}}/share" target="_blank" rel="noopener" data-busy="false"><button type="submit" rst-btn aria-label="Share meeting link">Share link</button></form>
55 <p><a href="/meetings/{{.Sqid}}/end" target="_blank" rel="noopener">End meeting for everyone</a></p>{{end}}
56</div></noscript>
57
58<div id="room" class="calls-room" data-sqid="{{.Sqid}}" data-boot="{{.Boot}}" data-self-name="{{.Name}}" data-is-member="{{if .IsMember}}1{{end}}" data-relay="{{if .Relay}}1{{end}}" data-peer="" hidden>
59 {{/* The notices: what stays on screen whatever the controls do, in a
60 column under the title (CallNotices on the Mac). The sharing
61 notice with its own Stop sharing (Paul, 2026-09-28: "I would
62 expect this notification to have a 'Stop sharing' button
63 inline"), and under it whose view of the screen is being
64 recovered (spec §15.21); "Click anywhere to turn on sound", which
65 room.js puts here; the call's own status line, with its way out
66 when the call is over; the relay warning room.js puts after it;
67 and each person waiting, with Admit. */}}
68 <div id="notices" class="calls-notices">
69 <div id="share-notice" class="calls-share-notice" hidden>
70 <p class="calls-share-notice-row">{{icon "screen-share"}}<span>You are sharing your screen</span><button type="button" id="share-stop" class="calls-share-stop" aria-label="Stop sharing your screen">Stop sharing</button></p>
71 <p id="share-recovery" class="calls-share-recovery" role="status" hidden></p>
72 </div>
73 <p id="status" class="calls-status" role="status"></p>
74 {{/* §5.5's relay notice, once a session: call-window.js shows it on
75 the first visit of a tab's session and it stays until it is put
76 away. */}}
77 {{if .Floor}}<div id="floor-notice" class="calls-floor-notice" hidden><p>Calls outside your network may not connect yet.</p><button type="button" id="floor-dismiss" aria-label="Dismiss the relay notice" title="Dismiss">{{icon "x"}}</button></div>{{end}}
78 {{/* iPhone and iPad (2026-10-03): Safari stops the camera and the
79 microphone, and lets the call's sound lapse, when the tab goes
80 to the background or the screen locks. call-window.js shows this
81 once a session on those devices, with its own Dismiss. */}}
82 <div id="keep-open-notice" class="calls-floor-notice" hidden><p>Keep this tab open and the screen on during the call.</p><button type="button" id="keep-open-dismiss" aria-label="Dismiss the keep-open notice" title="Dismiss">{{icon "x"}}</button></div>
83 <div id="queue" class="calls-queue" hidden><h2>Waiting to join</h2><ul id="knocks"></ul></div>
84 </div>
85
86 {{/* Unread chat, for anyone who cannot see the badge on the button.
87 The panel's own log is aria-live, but closing chat hides it, which
88 takes the whole region out of the accessibility tree — so a
89 message arriving while chat is closed announced nothing at all.
90 The badge is aria-hidden and a changed aria-label does not
91 announce either, so this was silent three ways. This region is
92 outside the panel and survives the closing. It carries the count
93 and never the message: someone who closed chat has said they do
94 not want it read out, and the count is exactly what the badge
95 beside the button shows. call-layout.js writes it only while
96 closed — with the panel open its own log announces arrivals, and
97 saying it twice is worse than once. */}}
98 <p id="chat-unread-live" class="calls-sr-only" role="status" aria-live="polite"></p>
99 {{/* Speaker view and the viewer's order (0.4.0): what call-layout.js
100 says when the person pins, unpins or moves someone. An automatic
101 change of speaker is never said: in a lively call it would talk
102 over the talking, and the stage tile says "On stage" itself. */}}
103 <p id="layout-live" class="calls-sr-only" role="status" aria-live="polite"></p>
104 {{/* Spec §15.19. Someone muting or turning their camera off, said
105 politely and once. A region of its own for the reason
106 #device-note has one: renderStatus rewrites #status on every
107 render, and would take the sentence away before it was read.
108 Present from the first paint, empty, so a reader is already
109 watching it when room.js first writes to it. */}}
110 <p id="media-live" class="calls-sr-only" role="status" aria-live="polite"></p>
111 <div id="stage" class="calls-stage">
112 <div id="tiles" class="calls-grid"></div>
113 </div>
114
115 {{/* The bar, in the Mac bar's order: microphone, camera, share,
116 devices, layout, share link, then chat, More and Leave at the
117 right. Glyphs, each with its name for a screen reader and a
118 tooltip; the word under each is shown in a phone's controls
119 sheet, where the bar is only More controls and Leave, as the
120 iPhone app has it. */}}
121 <div id="controls" class="calls-controls calls-fades" role="group" aria-label="Call controls">
122 <button type="button" id="more-controls" class="calls-bar-phone" aria-expanded="false" aria-controls="call-sheet" aria-label="More controls">{{icon "ellipsis"}}</button>
123 <div id="call-sheet" class="calls-sheet">
124 <button type="button" id="mute" rst-btn aria-label="Microphone" aria-pressed="false" data-tip="Mute microphone" data-tip-on="Unmute microphone"><span class="calls-g calls-g-rest">{{icon "mic"}}</span><span class="calls-g calls-g-pressed">{{icon "mic-off"}}</span><span class="calls-bar-label">Microphone<span id="mute-state" class="calls-toggle-state" aria-hidden="true">On</span></span></button>
125 <button type="button" id="camera" rst-btn aria-label="Camera" aria-pressed="false" data-tip="Turn camera off" data-tip-on="Turn camera on"><span class="calls-g calls-g-rest">{{icon "video"}}</span><span class="calls-g calls-g-pressed">{{icon "video-off"}}</span><span class="calls-bar-label">Camera<span id="camera-state" class="calls-toggle-state" aria-hidden="true">On</span></span></button>
126 <button type="button" id="share" rst-btn aria-pressed="false" aria-label="Share screen" data-tip="Share your screen" data-tip-on="Stop sharing your screen"><span class="calls-g calls-g-rest">{{icon "screen-share"}}</span><span class="calls-g calls-g-pressed">{{icon "screen-share-off"}}</span><span class="calls-bar-label">Share screen<span id="share-state" class="calls-toggle-state" aria-hidden="true">Off</span></span></button>
127 <button type="button" id="devices-toggle" rst-btn aria-expanded="false" aria-controls="room-devices" hidden aria-label="Devices" data-tip="Camera, microphone and speaker">{{icon "sliders-horizontal"}}<span class="calls-bar-label">Devices</span></button>
128 <button type="button" id="layout-toggle" rst-btn aria-expanded="false" aria-controls="layout-panel" aria-label="Video layout" data-tip="Video layout: Speaker or Grid" data-mode="speaker"><span class="calls-g calls-g-speaker">{{icon "panel-bottom"}}</span><span class="calls-g calls-g-grid">{{icon "layout-grid"}}</span><span class="calls-bar-label">Layout</span></button>
129 {{if .IsMember}}<form method="post" action="/m/{{.Sqid}}/share" target="_blank" rel="noopener" data-busy="false"><button type="submit" rst-btn aria-label="Share meeting link" data-tip="Share meeting link">{{icon "share"}}<span class="calls-bar-label">Share link</span></button></form>{{end}}
130 <span class="calls-bar-spacer" aria-hidden="true"></span>
131 <button type="button" id="toggle-chat" rst-btn aria-expanded="false" aria-controls="chat-overlay" hidden data-tip="Chat">{{icon "messages-square"}}<span class="calls-bar-label">Chat</span><span id="chat-unread" class="calls-badge" aria-hidden="true"></span></button>
132 <button type="button" id="call-more-toggle" rst-btn aria-expanded="false" aria-controls="call-more" aria-label="More" data-tip="More">{{icon "ellipsis"}}<span class="calls-bar-label">More</span></button>
133 </div>
134 <button type="button" id="leave" rst-btn="danger" data-tip="Leave meeting">{{icon "phone-down"}}<span class="calls-bar-label">Leave</span></button>
135 </div>
136
137 {{/* The bar's popovers, each over the picture above the button that
138 opened it: the layout, the devices, and More. call-window.js
139 places them and closes them (Escape, a click elsewhere). */}}
140 <div id="layout-panel" class="calls-popover" role="group" aria-label="Video layout" hidden>
141 <p class="calls-popover-title">Video layout</p>
142 <div id="video-layout" estilo-call-segmented role="group" aria-label="Video layout" hidden>
143 <button type="button" id="layout-speaker" aria-pressed="true">Speaker</button>
144 <button type="button" id="layout-grid" aria-pressed="false">Grid</button>
145 </div>
146 <p class="calls-popover-help">Speaker puts whoever is talking on the stage. Pin anyone from their tile.</p>
147 </div>
148 {{/* The device panel, closed until asked for. A call's own screen is
149 the video and the controls; three selects permanently over them
150 would be furniture the whole call pays for so that someone can
151 change a microphone once. room.js reveals the button — the panel
152 is useless in a browser that cannot enumerate devices — and fills
153 the selects from the same enumeration the holding screen uses.
154 The ids are devices.js's own on purpose: the two fill routines
155 are mirrors, and a reader comparing them should not also have to
156 translate names. */}}
157 <div id="room-devices" class="calls-pickers calls-popover calls-devices" role="group" aria-label="Devices" hidden>
158 <p class="calls-popover-title">Devices</p>
159 <div id="camera-row">{{template "field-select" dict "ID" "pick-camera" "Name" "camera" "Label" "Camera"}}</div>
160 <div id="mic-row">{{template "field-select" dict "ID" "pick-mic" "Name" "mic" "Label" "Microphone"}}</div>
161 <div id="speaker-row" hidden>{{template "field-select" dict "ID" "pick-speaker" "Name" "speaker" "Label" "Speaker"}}</div>
162 <div id="raw-row" class="calls-raw-row" hidden>{{template "field-check" dict "Name" "raw-audio" "Label" "Studio audio"}}
163 <p id="raw-help" class="calls-raw-help">Sends your microphone unprocessed and at a higher bitrate, for an audio interface in a room you have treated. It turns off echo cancellation, noise suppression and automatic gain, so leave it off on a laptop.</p></div>
164 {{/* §5.4: what a shared screen gives up first when the connection
165 cannot carry all of it. room.js fills the options, remembers the
166 choice and reveals the row in a browser that can share. */}}
167 <div id="share-mode-row" hidden>{{template "field-select" dict "ID" "pick-share-mode" "Name" "share-mode" "Label" "Optimise shared screen for" "Help" "Balanced keeps text readable and keeps up with scrolling. Text and detail keeps small print sharp; movement may look jerky. Motion and video keeps movement smooth; text may look softer."}}</div>
168 {{/* Device messages get a live region of their own, like the relay
169 warning: renderStatus owns #status and rewrites it on every
170 render, so a sentence about a camera that would not open has
171 to live somewhere that is not about to be overwritten. */}}
172 <p id="device-note" class="calls-device-note" role="status" hidden></p>
173 {{/* What the Mac panel's "Copy call diagnostics" copies, for the
174 browser: the connections, and the screen's recovery timeline and
175 decode statistics on both sides of a share (spec §15.22). Names,
176 states and counts only; room.js says in #device-note whether it
177 was copied. */}}
178 <button type="button" id="copy-diagnostics" rst-btn="ghost" class="calls-copy-diagnostics">Copy call diagnostics</button>
179 {{/* Spec §15.26: the same diagnostics, with the people named by
180 short peer id, sent to the server log with a note (report.js's
181 sheet); and the switch for the automatic anonymous quality
182 reports, on unless turned off in this browser. */}}
183 <button type="button" id="report-problem" rst-btn="ghost" class="calls-copy-diagnostics calls-report-problem">Report a problem</button>
184 <div id="quality-row" class="calls-quality-row">{{template "field-check" dict "Name" "quality-reports" "Label" "Send anonymous quality reports" "Checked" true}}
185 <p id="quality-help" class="calls-raw-help">Connection statistics every thirty seconds, to help fix call problems: no names, no audio or video.</p></div>
186 </div>
187 {{/* More: who you are in this call, then the account menu's items in
188 its order and its words (chrome.go, roomMenu) — Meetings,
189 Settings, Help → Open source, the one Sign out — which the title's
190 ☰ menu carried until 2026-10-02. Ending the meeting for everyone
191 is last, for a member: destructive, so a link to its own confirm
192 page (AGENTS.md), opened in a new tab so that following it does
193 not leave the call. With no script the header above has the same
194 navigation. */}}
195 <div id="call-more" class="calls-popover calls-menu" role="group" aria-label="More" hidden>
196 <p class="calls-menu-standing">You’re in this call as {{.Name}}{{if eq .Standing "guest"}}, a guest{{end}}.{{if .Starts}} Starts {{.Starts}}.{{end}}</p>
197 {{with .Menu.Identity}}<hr><p class="calls-menu-who"><span class="calls-menu-heading">Signed in as</span><span class="calls-menu-address">{{.}}</span></p>{{end}}
198 {{if .Menu.Nav}}<hr>{{range .Menu.Nav}}<a href="{{.Href}}">{{.Label}}</a>{{end}}{{end}}
199 <hr><p class="calls-menu-heading">Help</p>
200 {{range .Menu.Help}}<a href="{{.Href}}">{{.Label}}</a>{{end}}
201 {{/* §15.26: report.js's sheet, opened by room.js. */}}
202 <button type="button" id="report-problem-menu">Report a problem</button>
203 {{with .Menu.SignOut}}<hr><form method="post" action="{{.Action}}"><button type="submit">{{.Label}}</button></form>{{end}}
204 {{if .IsMember}}<hr><a href="/meetings/{{.Sqid}}/end" target="_blank" rel="noopener" class="calls-menu-danger">End meeting for everyone</a>{{end}}
205 </div>
206
207 {{/* Chat, as a sidebar sliding over the picture's right-hand side
208 (the Mac's chat sidebar): closed until asked for, and closing it
209 keeps the draft. The panel itself (spec §6) is one partial and
210 one script, and one comment in partials/chat.html; chat.js loads
211 before room.js in the head block, and two deferred scripts run in
212 document order, so window.Chat is there when room.js mounts it. */}}
213 {{/* One tile's menu (2026-09-29), opened from the ⋯ on a tile:
214 Pin, the three moves — the way to do without a drag what a drag
215 does, named for the person ("Move Ana earlier") — and, for a
216 member, Remove, a link to its own confirm page in a new tab. A
217 screen keeps only Pin. call-layout.js fills and places it. */}}
218 <div id="tile-menu" class="calls-menu calls-tile-menu" role="group" hidden>
219 <button type="button" data-act="pin">Pin</button>
220 <button type="button" data-move="first">Move to first</button>
221 <button type="button" data-move="earlier">Move earlier</button>
222 <button type="button" data-move="later">Move later</button>
223 <a class="calls-tile-remove calls-menu-danger" target="_blank" rel="noopener" hidden>Remove from the call</a>
224 </div>
225
226 <div id="chat-overlay" class="calls-chat-overlay" hidden>
227 <button type="button" id="chat-close" class="calls-chat-close" aria-label="Hide chat" title="Hide chat">{{icon "x"}}</button>
228 {{template "chat-panel" .}}
229 </div>
230
231 {{/* The two tile marks room.js clones onto each tile, rendered here so
232 they are the vendored badge and this app's own icons rather than a
233 copy of either in script. The badge's word is the state, for a
234 reader and for anyone who cannot tell a slashed microphone from a
235 plain one; the icon is on top of it, never instead. */}}
236 <template id="tile-muted">{{icon "mic-off"}}{{template "badge" dict "Label" "Muted"}}</template>
237 <template id="tile-camera-off">{{icon "video-off"}}<span>Camera off</span></template>
238 {{/* Each tile's arrangement buttons, which call-layout.js clones and
239 names: a Pinned badge while the tile is pinned, and ⋯, which opens
240 #tile-menu for that tile. */}}
241 <template id="tile-tools"><button type="button" class="calls-tool calls-tool-pin" data-act="pin" aria-pressed="true">{{icon "pin"}}<span class="calls-tool-text">Pinned</span></button><button type="button" class="calls-tool calls-tool-more" aria-expanded="false" aria-controls="tile-menu">{{icon "ellipsis"}}</button></template>
242</div>
243{{template "report-sheet" .}}
244</main>
245{{end}}
246