Implement the room name calculation algorithm

This commit is contained in:
Kegan Dougal 2021-10-08 17:24:06 +01:00
parent f9ed9ddd8c
commit 5ee1e422a1
2 changed files with 284 additions and 0 deletions

88
internal/roomname.go Normal file
View File

@ -0,0 +1,88 @@
package internal
import (
"fmt"
"strings"
)
type Hero struct {
ID string
Name string
}
func CalculateRoomName(roomName, canonicalAlias string, maxNumNamesPerRoom int, heroes []Hero, joinedCount, invitedCount int) string {
// If the room has an m.room.name state event with a non-empty name field, use the name given by that field.
if roomName != "" {
return roomName
}
// If the room has an m.room.canonical_alias state event with a valid alias field, use the alias given by that field as the name.
if canonicalAlias != "" {
return canonicalAlias
}
// If none of the above conditions are met, a name should be composed based on the members of the room.
disambiguatedNames := disambiguate(heroes)
totalNumOtherUsers := (joinedCount + invitedCount - 1)
isAlone := totalNumOtherUsers <= 0
// If m.joined_member_count + m.invited_member_count is less than or equal to 1 (indicating the member is alone),
// the client should use the rules BELOW to indicate that the room was empty. For example, "Empty Room (was Alice)",
// "Empty Room (was Alice and 1234 others)", or "Empty Room" if there are no heroes.
if len(heroes) == 0 && isAlone {
return "Empty Room"
}
// If the number of m.heroes for the room are greater or equal to m.joined_member_count + m.invited_member_count - 1,
// then use the membership events for the heroes to calculate display names for the users (disambiguating them if required)
// and concatenating them.
if len(heroes) >= totalNumOtherUsers {
if len(disambiguatedNames) == 1 {
return disambiguatedNames[0]
}
calculatedRoomName := strings.Join(disambiguatedNames[:len(disambiguatedNames)-1], ", ") + " and " + disambiguatedNames[len(disambiguatedNames)-1]
if isAlone {
return fmt.Sprintf("Empty Room (was %s)", calculatedRoomName)
}
return calculatedRoomName
}
// if we're here then len(heroes) < (joinedCount + invitedCount - 1)
numEntries := len(disambiguatedNames)
if numEntries > maxNumNamesPerRoom {
numEntries = maxNumNamesPerRoom
}
calculatedRoomName := fmt.Sprintf(
"%s and %d others", strings.Join(disambiguatedNames[:numEntries], ", "), totalNumOtherUsers-numEntries,
)
// If there are fewer heroes than m.joined_member_count + m.invited_member_count - 1,
// and m.joined_member_count + m.invited_member_count is greater than 1, the client should use the heroes to calculate
// display names for the users (disambiguating them if required) and concatenating them alongside a count of the remaining users.
if (joinedCount + invitedCount) > 1 {
return calculatedRoomName
}
// If m.joined_member_count + m.invited_member_count is less than or equal to 1 (indicating the member is alone),
// the client should use the rules above to indicate that the room was empty. For example, "Empty Room (was Alice)",
// "Empty Room (was Alice and 1234 others)", or "Empty Room" if there are no heroes.
return fmt.Sprintf("Empty Room (was %s)", calculatedRoomName)
}
func disambiguate(heroes []Hero) []string {
displayNames := make(map[string][]int)
for i, h := range heroes {
displayNames[h.Name] = append(displayNames[h.Name], i)
}
disambiguatedNames := make([]string, len(heroes))
for _, indexes := range displayNames {
if len(indexes) == 1 {
disambiguatedNames[indexes[0]] = heroes[indexes[0]].Name
continue
}
// disambiguate all these heroes
for _, i := range indexes {
h := heroes[i]
disambiguatedNames[i] = fmt.Sprintf("%s (%s)", h.Name, h.ID)
}
}
return disambiguatedNames
}

196
internal/roomname_test.go Normal file
View File

@ -0,0 +1,196 @@
package internal
import "testing"
func TestCalculateRoomName(t *testing.T) {
testCases := []struct {
roomName string
canonicalAlias string
heroes []Hero
joinedCount int
invitedCount int
maxNumNamesPerRoom int
wantRoomName string
}{
// Room name takes precedence
{
roomName: "My Room Name",
canonicalAlias: "#alias:localhost",
joinedCount: 5,
invitedCount: 1,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
{
ID: "@bob:localhost",
Name: "Bob",
},
},
wantRoomName: "My Room Name",
},
// Alias takes precedence if room name is missing
{
canonicalAlias: "#alias:localhost",
joinedCount: 5,
invitedCount: 1,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
{
ID: "@bob:localhost",
Name: "Bob",
},
},
wantRoomName: "#alias:localhost",
},
// ... and N others (large group chat)
{
joinedCount: 5,
invitedCount: 1,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
{
ID: "@bob:localhost",
Name: "Bob",
},
},
wantRoomName: "Alice, Bob and 3 others",
},
// Small group chat
{
joinedCount: 4,
invitedCount: 0,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
{
ID: "@bob:localhost",
Name: "Bob",
},
{
ID: "@charlie:localhost",
Name: "Charlie",
},
},
wantRoomName: "Alice, Bob and Charlie",
},
// DM room
{
joinedCount: 2,
invitedCount: 0,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
},
wantRoomName: "Alice",
},
// 3-way room
{
joinedCount: 3,
invitedCount: 0,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
{
ID: "@bob:localhost",
Name: "Bob",
},
},
wantRoomName: "Alice and Bob",
},
// disambiguation all
{
joinedCount: 10,
invitedCount: 0,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
{
ID: "@bob:localhost",
Name: "Alice",
},
{
ID: "@charlie:localhost",
Name: "Alice",
},
},
wantRoomName: "Alice (@alice:localhost), Alice (@bob:localhost), Alice (@charlie:localhost) and 6 others",
},
// disambiguation some
{
joinedCount: 10,
invitedCount: 0,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
{
ID: "@bob:localhost",
Name: "Bob",
},
{
ID: "@charlie:localhost",
Name: "Alice",
},
},
wantRoomName: "Alice (@alice:localhost), Bob, Alice (@charlie:localhost) and 6 others",
},
// left room
{
joinedCount: 1,
invitedCount: 0,
maxNumNamesPerRoom: 3,
heroes: []Hero{
{
ID: "@alice:localhost",
Name: "Alice",
},
{
ID: "@bob:localhost",
Name: "Bob",
},
},
wantRoomName: "Empty Room (was Alice and Bob)",
},
// empty room
{
joinedCount: 1,
invitedCount: 0,
maxNumNamesPerRoom: 3,
heroes: []Hero{},
wantRoomName: "Empty Room",
},
}
for _, tc := range testCases {
gotName := CalculateRoomName(tc.roomName, tc.canonicalAlias, tc.maxNumNamesPerRoom, tc.heroes, tc.joinedCount, tc.invitedCount)
if gotName != tc.wantRoomName {
t.Errorf("got %s want %s for test case: %+v", gotName, tc.wantRoomName, tc)
}
}
}