Keep the in-call Share button and the scriptless one
a6c2ac8 fixed the scriptless regression by moving the Share form back out of #room, which also undid the control-row placement c80a506 introduced for parity with the native call bar. That traded one defect for a smaller one. This keeps both. The form now renders twice and never both at once. The control-row copy is inside #room, which room.js un-hides, so it is the scripted path. The <noscript> copy is the scriptless path: <noscript> is parsed as markup only when scripting is off, so exactly one of the two is ever real, and the sentence promising "everything else on this page works without it" is true again — rather than made true by removing the feature it describes. Worth recording why the earlier reading was wrong, since it nearly stood. The 2026-09-12 spec note attributes "Share in the meeting view" to Paul, which looked like it made the control-row placement his requirement. It does not: ruling 16 was the invitation-token show-once rule, and what the note supersedes is retrievability — that a member can get a link back later rather than only at creation. At b6e8f29 the form was already on the room page, above #room, so the request was satisfied before c80a506 moved it. The placement inside the control row is design judgement, not the ask. The regression test needed no change and now covers a third layout neither version had. I checked it both ways again: it passes with the two copies, and still fails when the <noscript> copy is deleted and only the control-row one remains. GOTOOLCHAIN=go1.26.5 make ci passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 files changed,
+25
−10
internal/calls/templates/room.html+9 −2internal/callstest/room_test.go+16 −8
CI failed — run details
diff --git a/internal/calls/templates/room.html b/internal/calls/templates/room.html| index edfda3d..56cd2fa 100644 |
| --- a/internal/calls/templates/room.html |
| +++ b/internal/calls/templates/room.html |
| @@ -18,8 +18,14 @@ |
| <p>You are {{.Name}}, joining as a {{.Standing}}.</p> |
| {{if .Floor}}<p rst-notice>This team's Calls has no relay yet. Calls usually connect only between people on the same network.</p>{{end}} |
| -{{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>{{end}} |
| -<noscript><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></noscript> |
| +{{/* The Share form appears twice on purpose, and never at the same |
| + time. The control-row copy below is inside #room, which room.js |
| + un-hides, so it is the scripted path and matches the native call |
| + bar. This copy is the scriptless one: <noscript> is parsed as |
| + markup only when scripting is off, so exactly one of the two is |
| + ever real. Without it the sentence above is false, which is how |
| + it stood between c80a506 and a6c2ac8. */}} |
| +<noscript><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>{{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>{{end}}</noscript> |
| <div id="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> |
| <div id="call-workspace" estilo-call-workspace="sidebar"> |
| <div class="calls-stage"> |
| @@ -30,6 +36,7 @@ |
| <button type="button" id="camera" rst-btn aria-label="Camera" aria-pressed="false">{{icon "video"}}Camera<span id="camera-state" class="calls-toggle-state" aria-hidden="true">On</span></button> |
| <button type="button" id="share" rst-btn aria-pressed="false">{{icon "monitor"}}Share screen<span id="share-state" class="calls-toggle-state" aria-hidden="true">Off</span></button> |
| <button type="button" id="devices-toggle" rst-btn aria-expanded="false" aria-controls="room-devices" hidden>{{icon "sliders-horizontal"}}Devices</button> |
| + {{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>{{end}} |
| <button type="button" id="toggle-chat" rst-btn aria-expanded="true" aria-controls="chat" hidden>{{icon "message-circle"}}Chat<span id="chat-unread" aria-hidden="true"></span></button> |
| <button type="button" id="leave" rst-btn="danger">{{icon "log-out"}}Leave</button> |
| </div> |
diff --git a/internal/callstest/room_test.go b/internal/callstest/room_test.go| index c98f23e..570ebe6 100644 |
| --- a/internal/callstest/room_test.go |
| +++ b/internal/callstest/room_test.go |
| @@ -42,14 +42,22 @@ func TestRoomPageRendersItsHeaderStandingAndScriptlessFallback(t *testing.T) { |
| if headEnd < 0 || css < 0 || css > headEnd { |
| t.Fatalf("calls.css is not inside <head>: head ends at %d, calls.css at %d\n%s", headEnd, css, body) |
| } |
| - // The member Share-link form must sit outside #room, which is hidden |
| - // until room.js runs. It lived inside #controls between c80a506 and |
| - // 03588e2, which put a member action behind JavaScript while the |
| - // <noscript> paragraph directly above went on promising "everything |
| - // else on this page works without it". AGENTS.md makes that floor |
| - // load-bearing with two named exceptions, and sharing is not one of |
| - // them. Position, not presence, is what regressed: the form rendered |
| - // either way, so any Contains check would have stayed green. |
| + // A member must be able to reach Share without JavaScript. The form |
| + // is rendered twice and never both at once: the control-row copy is |
| + // inside #room, which room.js un-hides, and the <noscript> copy is |
| + // the scriptless path. This asserts the first occurrence precedes |
| + // #room, so deleting the <noscript> copy and leaving only the |
| + // control-row one fails. |
| + // |
| + // Between c80a506 and a6c2ac8 only the control-row copy existed, so |
| + // a member action sat behind JavaScript while the <noscript> |
| + // paragraph went on promising "everything else on this page works |
| + // without it". AGENTS.md makes that floor load-bearing with two |
| + // named exceptions, and sharing is not one of them. |
| + // |
| + // Position, not presence, is what regressed: the form rendered in |
| + // both layouts, so every Contains-style assertion stayed green. |
| + // That is why this compares offsets. |
| share := strings.Index(body, `action="/m/`+sqid+`/share"`) |
| room := strings.Index(body, `<div id="room"`) |
| if share < 0 || room < 0 || share > room { |