An invite that replaced a request to join was read as somebody else being
mid-join. Nobody always is: a host without auto-join, a join that failed, or
a fresh load already at that membership left the user waiting with nothing to
press but "Cancel request". The lobby now takes such an invite up itself,
showing a joining state, and offers a join of the user's own if that fails.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Running as a widget, Element Call has no access token of its own, so a room
it is not in can only be entered by the host: the lobby ended at a
full-screen "Room not found" for every room the user had not already joined.
Membership changes now go through a `ChangeMembership` function, which the
host supplies when it keeps the access token to itself and Element Call
builds from its own client when it does not. Either way the answer is the
membership the request resulted in, which for the client is the one an
accepted request implies: local room state still holds the previous
membership until the next sync. The host takes an `io.element.membership`
action carrying `{action: "join" | "knock" | "cancel_knock", reason?}`. An
error reply carrying `matrix_api_error` is the homeserver's own refusal; one
without it means the host does not implement the action, which leaves the
lobby saying the user cannot get in this way. Element Call also asks to
receive `m.room.join_rules`, and reads the state event rather than
`Room.getJoinRule()`, since a host that grants no such capability is
otherwise indistinguishable from an invite-only room.
What the lobby opens on comes from our membership, the membership it
replaced and the join rule. A restricted or knock-restricted room offers a
join, because the widget cannot evaluate `allowed_room_ids` and the host can.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`LobbyView` took `onEnter`, `enterLabel` and `waitingForInvite`, which cannot
express a declined, banned or invite-only room, so each of those ended at a
full-screen error and took the camera preview with it.
It now takes a `LobbyJoinState`, one of seven kinds, and renders both the
button and a message under the preview from it. `KnockLobbyView` passes
through what the loader offers, which is a single `lobby` state in place of
`canKnock` and `waitForInvite`, so a decline, a ban, or a room that takes
neither joins nor knocks keeps the user in the lobby. Waiting says what it is
waiting for and offers a way to withdraw the request.
The loader drives that state from the lobby's own callbacks rather than from
the load promise, so a refused request leaves the user where they were, and
every membership listener it adds is dropped when the effect is cleaned up.
What it knows of a room nobody has joined is a `PreJoinRoomInfo`, built from
a summary or from a `Room`, so a ban Element Call finds at load time is a
lobby state as well.
Denied and banned reuse the translated `group_call_loader.*` copy, so four
strings are new.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`registerRequest({})` rejects with the registration flows, and a homeserver
may answer with no `params` at all, which threw a TypeError on the landing
and login screens and left them without a privacy policy link.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
EC_DEV_TLS_KEY / EC_DEV_TLS_CERT point vite at a key pair the developer's
browser already trusts, instead of the checked-in Element Call Dev CA.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>