Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only emit avatars when DM room is set #394

Merged
merged 3 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 2 additions & 23 deletions internal/roomname.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,6 @@ func (m *RoomMetadata) SameRoomName(other *RoomMetadata) bool {
sameHeroNames(m.Heroes, other.Heroes))
}

// SameRoomAvatar checks if the fields relevant for room avatars have changed between the two metadatas.
// Returns true if there are no changes.
func (m *RoomMetadata) SameRoomAvatar(other *RoomMetadata) bool {
return m.AvatarEvent == other.AvatarEvent && sameHeroAvatars(m.Heroes, other.Heroes)
}

func (m *RoomMetadata) SameJoinCount(other *RoomMetadata) bool {
return m.JoinCount == other.JoinCount
}
Expand All @@ -138,21 +132,6 @@ func sameHeroNames(a, b []Hero) bool {
return true
}

func sameHeroAvatars(a, b []Hero) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].ID != b[i].ID {
return false
}
if a[i].Avatar != b[i].Avatar {
return false
}
}
return true
}

func (m *RoomMetadata) RemoveHero(userID string) {
for i, h := range m.Heroes {
if h.ID == userID {
Expand Down Expand Up @@ -264,11 +243,11 @@ const noAvatar = ""
// CalculateAvatar computes the avatar for the room, based on the global room metadata.
// Assumption: metadata.RemoveHero has been called to remove the user who is syncing
// from the list of heroes.
func CalculateAvatar(metadata *RoomMetadata) string {
func CalculateAvatar(metadata *RoomMetadata, isDM bool) string {
if metadata.AvatarEvent != "" {
return metadata.AvatarEvent
}
if len(metadata.Heroes) == 1 {
if len(metadata.Heroes) == 1 && isDM {
return metadata.Heroes[0].Avatar
}
return noAvatar
Expand Down
2 changes: 1 addition & 1 deletion sync3/caches/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func NewInviteData(ctx context.Context, userID, roomID string, inviteState []jso
Timestamp: uint64(ts),
AlwaysProcess: true,
}
id.IsDM = j.Get("is_direct").Bool()
id.IsDM = j.Get("content.is_direct").Bool()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just a general bug.

} else if target == j.Get("sender").Str {
id.Heroes = append(id.Heroes, internal.Hero{
ID: target,
Expand Down
2 changes: 1 addition & 1 deletion sync3/handler/connstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ func (s *ConnState) getInitialRoomData(ctx context.Context, roomSub sync3.RoomSu
roomName, calculated := internal.CalculateRoomName(metadata, 5) // TODO: customisable?
room := sync3.Room{
Name: roomName,
AvatarChange: sync3.NewAvatarChange(internal.CalculateAvatar(metadata)),
AvatarChange: sync3.NewAvatarChange(internal.CalculateAvatar(metadata, userRoomData.IsDM)),
NotificationCount: int64(userRoomData.NotificationCount),
HighlightCount: int64(userRoomData.HighlightCount),
Timeline: roomToTimeline[roomID],
Expand Down
2 changes: 1 addition & 1 deletion sync3/handler/connstate_live.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ func (s *connStateLive) processLiveUpdate(ctx context.Context, up caches.Update,
if delta.RoomAvatarChanged {
metadata := roomUpdate.GlobalRoomMetadata()
metadata.RemoveHero(s.userID)
thisRoom.AvatarChange = sync3.NewAvatarChange(internal.CalculateAvatar(metadata))
thisRoom.AvatarChange = sync3.NewAvatarChange(internal.CalculateAvatar(metadata, roomUpdate.UserRoomMetadata().IsDM))
}
if delta.InviteCountChanged {
thisRoom.InvitedCount = &roomUpdate.GlobalRoomMetadata().InviteCount
Expand Down
6 changes: 3 additions & 3 deletions sync3/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ func (s *InternalRequestLists) SetRoom(r RoomConnMetadata) (delta RoomDelta) {
// to conclude.
r.CanonicalisedName = existing.CanonicalisedName
}
delta.RoomAvatarChanged = !existing.SameRoomAvatar(&r.RoomMetadata)
delta.RoomAvatarChanged = !existing.SameRoomAvatar(&r)
if delta.RoomAvatarChanged {
r.ResolvedAvatarURL = internal.CalculateAvatar(&r.RoomMetadata)
r.ResolvedAvatarURL = internal.CalculateAvatar(&r.RoomMetadata, r.IsDM)
}

// Interpret the timestamp map on r as the changes we should apply atop the
Expand All @@ -114,7 +114,7 @@ func (s *InternalRequestLists) SetRoom(r RoomConnMetadata) (delta RoomDelta) {
r.CanonicalisedName = strings.ToLower(
strings.Trim(roomName, "#!():_@"),
)
r.ResolvedAvatarURL = internal.CalculateAvatar(&r.RoomMetadata)
r.ResolvedAvatarURL = internal.CalculateAvatar(&r.RoomMetadata, r.IsDM)
// We'll automatically use the LastInterestedEventTimestamps provided by the
// caller, so that recency sorts work.
}
Expand Down
31 changes: 31 additions & 0 deletions sync3/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,37 @@ type RoomConnMetadata struct {
LastInterestedEventTimestamps map[string]uint64
}

// SameRoomAvatar checks if the fields relevant for room avatars have changed between the two metadatas.
// Returns true if there are no changes.
func (r *RoomConnMetadata) SameRoomAvatar(next *RoomConnMetadata) bool {
sameRoomAvatar := r.AvatarEvent == next.AvatarEvent
if next.IsDM {
// the avatar is the same IF:
// - the m.room.avatar event is the same AND
// - the heroes haven't changed AND
// - the number of heroes is 1
return sameRoomAvatar && sameHeroAvatars(r.Heroes, next.Heroes) && len(next.Heroes) == 1
}
// the avatar is the same IF:
// - the m.room.avatar event is the same
return sameRoomAvatar
}

func sameHeroAvatars(a, b []internal.Hero) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].ID != b[i].ID {
return false
}
if a[i].Avatar != b[i].Avatar {
return false
}
}
return true
}

func (r *RoomConnMetadata) GetLastInterestedEventTimestamp(listKey string) uint64 {
ts, ok := r.LastInterestedEventTimestamps[listKey]
if ok {
Expand Down
101 changes: 93 additions & 8 deletions tests-e2e/lists_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,11 @@ func TestAvatarFieldInRoomResponse(t *testing.T) {
"invite": []string{bob.UserID, chris.UserID},
})

alice.MustSetGlobalAccountData(t, "m.direct", map[string]any{
bob.UserID: []string{dmBob, dmBobChris},
chris.UserID: []string{dmChris, dmBobChris},
})

t.Logf("Rooms:\npublic=%s\ndmBob=%s\ndmChris=%s\ndmBobChris=%s", public, dmBob, dmChris, dmBobChris)
t.Log("Bob accepts his invites. Chris accepts none.")
bob.JoinRoom(t, dmBob, nil)
Expand All @@ -1390,14 +1395,14 @@ func TestAvatarFieldInRoomResponse(t *testing.T) {
},
})

t.Log("Alice should see each room in the sync response with an appropriate avatar")
t.Log("Alice should see each room in the sync response with an appropriate avatar and DM flag")
m.MatchResponse(
t,
res,
m.MatchRoomSubscription(public, m.MatchRoomUnsetAvatar()),
m.MatchRoomSubscription(dmBob, m.MatchRoomAvatar(bob.AvatarURL)),
m.MatchRoomSubscription(dmChris, m.MatchRoomAvatar(chris.AvatarURL)),
m.MatchRoomSubscription(dmBobChris, m.MatchRoomUnsetAvatar()),
m.MatchRoomSubscription(public, m.MatchRoomUnsetAvatar(), m.MatchRoomIsDM(false)),
m.MatchRoomSubscription(dmBob, m.MatchRoomAvatar(bob.AvatarURL), m.MatchRoomIsDM(true)),
m.MatchRoomSubscription(dmChris, m.MatchRoomAvatar(chris.AvatarURL), m.MatchRoomIsDM(true)),
m.MatchRoomSubscription(dmBobChris, m.MatchRoomUnsetAvatar(), m.MatchRoomIsDM(true)),
)

t.Run("Avatar not resent on message", func(t *testing.T) {
Expand Down Expand Up @@ -1713,7 +1718,6 @@ func TestAvatarFieldInRoomResponse(t *testing.T) {
}
return m.MatchRoomAvatar(bob.AvatarURL)(r)
}))

})

t.Run("See avatar when invited", func(t *testing.T) {
Expand All @@ -1727,7 +1731,88 @@ func TestAvatarFieldInRoomResponse(t *testing.T) {
t.Log("Alice syncs until she sees the invite.")
res = alice.SlidingSyncUntilMembership(t, res.Pos, dmInvited, alice, "invite")

t.Log("The new room should use Chris's avatar.")
m.MatchResponse(t, res, m.MatchRoomSubscription(dmInvited, m.MatchRoomAvatar(chris.AvatarURL)))
t.Log("The new room should appear as a DM and use Chris's avatar.")
m.MatchResponse(t, res, m.MatchRoomSubscription(dmInvited, m.MatchRoomIsDM(true), m.MatchRoomAvatar(chris.AvatarURL)))

t.Run("Creator of a non-DM never sees an avatar", func(t *testing.T) {
t.Log("Alice makes a new room which is not a DM.")
privateGroup := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "trusted_private_chat",
"is_direct": false,
})

t.Log("Alice sees the group. It has no avatar.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, m.MatchRoomSubscription(privateGroup, m.MatchRoomUnsetAvatar()))
m.MatchResponse(t, res, m.MatchRoomSubscription(privateGroup, m.MatchRoomIsDM(false)))

t.Log("Alice invites Bob to the group, who accepts.")
alice.MustInviteRoom(t, privateGroup, bob.UserID)
bob.MustJoinRoom(t, privateGroup, nil)

t.Log("Alice sees Bob join. The room still has no avatar.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, func(response *sync3.Response) error {
matchNoAvatarChange := m.MatchRoomSubscription(privateGroup, m.MatchRoomUnchangedAvatar())
if err := matchNoAvatarChange(response); err != nil {
t.Fatalf("Saw group avatar change: %s", err)
}
matchJoin := m.MatchRoomSubscription(privateGroup, MatchRoomTimelineMostRecent(1, []Event{
{
Type: "m.room.member",
Sender: bob.UserID,
StateKey: ptr(bob.UserID),
},
}))
return matchJoin(response)
})

t.Log("Alice invites Chris to the group, who accepts.")
alice.MustInviteRoom(t, privateGroup, chris.UserID)
chris.MustJoinRoom(t, privateGroup, nil)

t.Log("Alice sees Chris join. The room still has no avatar.")
res = alice.SlidingSyncUntil(t, res.Pos, sync3.Request{}, func(response *sync3.Response) error {
matchNoAvatarChange := m.MatchRoomSubscription(privateGroup, m.MatchRoomUnchangedAvatar())
if err := matchNoAvatarChange(response); err != nil {
t.Fatalf("Saw group avatar change: %s", err)
}
matchJoin := m.MatchRoomSubscription(privateGroup, MatchRoomTimelineMostRecent(1, []Event{
{
Type: "m.room.member",
Sender: chris.UserID,
StateKey: ptr(chris.UserID),
},
}))
return matchJoin(response)
})
})
})
}

// Regression test for https://github.com/element-hq/element-x-ios/issues/2003
// Ensure that a group chat with 1 other person has no avatar field set. Only DMs should have this set.
func TestAvatarUnsetInTwoPersonRoom(t *testing.T) {
alice := registerNamedUser(t, "alice")
bob := registerNamedUser(t, "bob")
bobAvatar := alice.UploadContent(t, smallPNG, "bob.png", "image/png")
bob.SetAvatar(t, bobAvatar)
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"preset": "trusted_private_chat",
"name": "Nice test room",
"invite": []string{bob.UserID},
})

res := alice.SlidingSync(t, sync3.Request{
Lists: map[string]sync3.RequestList{
"a": {
Ranges: sync3.SliceRanges{{0, 20}},
},
},
})
m.MatchResponse(t, res, m.MatchRoomSubscriptionsStrict(map[string][]m.RoomMatcher{
roomID: {
m.MatchRoomUnsetAvatar(),
m.MatchInviteCount(1),
m.MatchRoomName("Nice test room"),
},
}))
}
9 changes: 9 additions & 0 deletions testutils/m/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ func MatchRoomUnchangedAvatar() RoomMatcher {
}
}

func MatchRoomIsDM(wantDM bool) RoomMatcher {
return func(r sync3.Room) error {
if r.IsDM != wantDM {
return fmt.Errorf("MatchRoomIsDM: got %t want %t", r.IsDM, wantDM)
}
return nil
}
}

func MatchJoinCount(count int) RoomMatcher {
return func(r sync3.Room) error {
if r.JoinedCount != count {
Expand Down
Loading