| 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 | |