Web client (warp-web) with E2EE, and two sync fixes
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
apps/web: a Matrix client for the browser on Vue 3, Tailwind and matrix-js-sdk, built on @warp/ui. Like the admin panel it is an optional, separate service: warp-web embeds the built app, serves /config.json and proxies only /_matrix/client and /_matrix/media to the homeserver (or, with -public-homeserver, lets the browser call it directly). Sign-in and sign-up with registration tokens, room list, timeline with back-pagination, text, images, video and files over authenticated media, local echo with sent/read/failed states, receipts, typing, invites, creating, joining and leaving rooms, room info, settings, phone layout. End-to-end encryption through the Rust crypto: encrypted messages and files, emoji verification, key backup setup and restore. apps/ui: WRoomItem, WMessageBubble and WComposer from the brand package, and the icons the client needs. syncapi fixes found with the client: - A room joined inside the sync window was sent as the join alone with limited: false, so clients thought they had the whole room. It is now read like an initial sync; a profile change is not mistaken for a join. - A live typing notification was reported on every sync, so every long poll returned at once while anybody typed. Typing now has a position in the sync token and is reported when it changes, including the empty list when the last person stops. Also: Dockerfile, a `web` compose profile, Makefile targets, a CI job with a proxy smoke test, docs. PLAN.md moves out of the repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
adc18e7cf3
commit
b6f3be6ac4
90 files changed
+6608
-472
No files matched your search
@@ -320,6 +320,7 @@ func (s *API) build(ctx context.Context, req SyncRequest, filter Filter, since T
|
||||
return Response{}, false, err
|
||||
}
|
||||
to := tokenAt(positions)
|
||||
to.Typing = s.typing.position()
|
||||
|
||||
resp := Response{
|
||||
Rooms: Rooms{
|
||||
@@ -443,6 +444,13 @@ func (s *API) buildRooms(ctx context.Context, req SyncRequest, filter Filter, si
|
||||
to: to,
|
||||
full: since.IsInitial() || req.FullState || room.Position > since.PDU,
|
||||
}
|
||||
if room.Membership == event.MembershipJoin && !since.IsInitial() && room.Position > since.PDU {
|
||||
joined, err := s.isNewJoin(ctx, req, room)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
window.joined = joined
|
||||
}
|
||||
|
||||
switch room.Membership {
|
||||
case event.MembershipJoin:
|
||||
@@ -490,6 +498,23 @@ type roomWindow struct {
|
||||
// the client's position: an initial sync, ?full_state=true, or a room
|
||||
// the user has only just joined.
|
||||
full bool
|
||||
|
||||
// joined is a room the user joined inside the window. To their client
|
||||
// the room is new, so it is read as an initial sync reads it: the
|
||||
// recent history they may see and the state before it, not only the
|
||||
// events since the client's position, which would be their own join.
|
||||
joined bool
|
||||
}
|
||||
|
||||
// isNewJoin reports whether the user's membership event inside the window
|
||||
// is a join into the room, rather than a profile change of a member who
|
||||
// was already in it.
|
||||
func (s *API) isNewJoin(ctx context.Context, req SyncRequest, room roomserver.MembershipAt) (bool, error) {
|
||||
before, err := s.rooms.MembershipBefore(ctx, room.RoomID, req.User, room.EventID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return before != event.MembershipJoin, nil
|
||||
}
|
||||
|
||||
// from is the position a stream is read from, which is the start of it for
|
||||
@@ -501,6 +526,15 @@ func (w roomWindow) from(pos int64) int64 {
|
||||
return pos
|
||||
}
|
||||
|
||||
// timelineSince is the position a room's timeline and state are read from:
|
||||
// the client's, or the start of the room for one joined in the window.
|
||||
func (w roomWindow) timelineSince() int64 {
|
||||
if w.joined {
|
||||
return 0
|
||||
}
|
||||
return w.since.PDU
|
||||
}
|
||||
|
||||
// changedRooms is the set of rooms this sync has to look at.
|
||||
//
|
||||
// On an initial sync that is all of them. On an incremental one it is the
|
||||
@@ -543,12 +577,13 @@ func (s *API) changedRooms(ctx context.Context, req SyncRequest, filter Filter,
|
||||
}
|
||||
}
|
||||
|
||||
// Typing lives in memory and has no position at all: a notification
|
||||
// that started and stopped between two syncs is simply never reported,
|
||||
// and one that is live is reported every time until it expires.
|
||||
for _, roomID := range s.typing.activeRooms() {
|
||||
if slices.Contains(joined, roomID) {
|
||||
out[roomID] = struct{}{}
|
||||
// Typing lives in memory, with a position of its own that counts
|
||||
// changes: a room is looked at when its typing changed in the window.
|
||||
if to.Typing != since.Typing {
|
||||
for _, roomID := range s.typing.changedRooms(since.Typing, to.Typing) {
|
||||
if slices.Contains(joined, roomID) {
|
||||
out[roomID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,7 +673,7 @@ func (s *API) roomSyncRequest(req SyncRequest, filter Filter, window roomWindow,
|
||||
return roomserver.RoomSyncRequest{
|
||||
RoomID: room.RoomID,
|
||||
User: req.User,
|
||||
Since: window.since.PDU,
|
||||
Since: window.timelineSince(),
|
||||
To: window.to.PDU,
|
||||
TimelineLimit: filter.TimelineLimit(),
|
||||
TimelineFilter: filter.TimelineFilter(),
|
||||
@@ -667,8 +702,17 @@ func (s *API) roomEphemeral(ctx context.Context, userID, roomID string, window r
|
||||
out := []EphemeralEvent{}
|
||||
ephemeralFilter := filter.EphemeralFilter()
|
||||
|
||||
if e := s.typingEvent(roomID); e != nil && matchesEventFilter(e.Type, "", ephemeralFilter) {
|
||||
out = append(out, *e)
|
||||
// A full read reports whoever is typing now; an incremental one
|
||||
// reports the room's list when it changed, even when it is now empty.
|
||||
var typing *EphemeralEvent
|
||||
if window.full {
|
||||
typing = s.typingEvent(roomID, false)
|
||||
} else if window.to.Typing != window.since.Typing &&
|
||||
s.typing.changedIn(roomID, window.since.Typing, window.to.Typing) {
|
||||
typing = s.typingEvent(roomID, true)
|
||||
}
|
||||
if typing != nil && matchesEventFilter(typing.Type, "", ephemeralFilter) {
|
||||
out = append(out, *typing)
|
||||
}
|
||||
|
||||
byRoom, err := s.roomReceipts(ctx, userID, []string{roomID},
|
||||
|
||||
Reference in new issue
Block a user