Skip to content

Commit eb79f62

Browse files
authored
Add ability to override built in room name generator for an i18n'able one (#2609)
* Add ability to override built in room name generator for an i18n'able one * Add tests
1 parent 4b1a443 commit eb79f62

File tree

3 files changed

+111
-16
lines changed

3 files changed

+111
-16
lines changed

spec/unit/room.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2545,4 +2545,16 @@ describe("Room", function() {
25452545
});
25462546
});
25472547
});
2548+
2549+
describe("roomNameGenerator", () => {
2550+
const client = new TestClient(userA).client;
2551+
client.roomNameGenerator = jest.fn().mockReturnValue(null);
2552+
const room = new Room(roomId, client, userA);
2553+
2554+
it("should call fn when recalculating room name", () => {
2555+
(client.roomNameGenerator as jest.Mock).mockClear();
2556+
room.recalculate();
2557+
expect(client.roomNameGenerator).toHaveBeenCalled();
2558+
});
2559+
});
25482560
});

src/client.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ import { VerificationRequest } from "./crypto/verification/request/VerificationR
137137
import { VerificationBase as Verification } from "./crypto/verification/Base";
138138
import * as ContentHelpers from "./content-helpers";
139139
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
140-
import { Room } from "./models/room";
140+
import { Room, RoomNameState } from "./models/room";
141141
import {
142142
IAddThreePidOnlyBody,
143143
IBindThreePidBody,
@@ -344,6 +344,12 @@ export interface ICreateClientOpts {
344344
fallbackICEServerAllowed?: boolean;
345345

346346
cryptoCallbacks?: ICryptoCallbacks;
347+
348+
/**
349+
* Method to generate room names for empty rooms and rooms names based on membership.
350+
* Defaults to a built-in English handler with basic pluralisation.
351+
*/
352+
roomNameGenerator?: (roomId: string, state: RoomNameState) => string | null;
347353
}
348354

349355
export interface IMatrixClientCreateOpts extends ICreateClientOpts {
@@ -918,6 +924,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
918924
protected fallbackICEServerAllowed = false;
919925
protected roomList: RoomList;
920926
protected syncApi: SlidingSyncSdk | SyncApi;
927+
public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"];
921928
public pushRules: IPushRules;
922929
protected syncLeftRoomsPromise: Promise<Room[]>;
923930
protected syncedLeftRooms = false;
@@ -1041,6 +1048,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
10411048
// we still want to know which rooms are encrypted even if crypto is disabled:
10421049
// we don't want to start sending unencrypted events to them.
10431050
this.roomList = new RoomList(this.cryptoStore);
1051+
this.roomNameGenerator = opts.roomNameGenerator;
10441052

10451053
this.toDeviceMessageQueue = new ToDeviceMessageQueue(this);
10461054

src/models/room.ts

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2914,6 +2914,33 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
29142914
return this.getType() === RoomType.ElementVideo;
29152915
}
29162916

2917+
private roomNameGenerator(state: RoomNameState): string {
2918+
if (this.client.roomNameGenerator) {
2919+
const name = this.client.roomNameGenerator(this.roomId, state);
2920+
if (name !== null) {
2921+
return name;
2922+
}
2923+
}
2924+
2925+
switch (state.type) {
2926+
case RoomNameType.Actual:
2927+
return state.name;
2928+
case RoomNameType.Generated:
2929+
switch (state.subtype) {
2930+
case "Inviting":
2931+
return `Inviting ${memberNamesToRoomName(state.names, state.count)}`;
2932+
default:
2933+
return memberNamesToRoomName(state.names, state.count);
2934+
}
2935+
case RoomNameType.EmptyRoom:
2936+
if (state.oldName) {
2937+
return `Empty room (was ${state.oldName})`;
2938+
} else {
2939+
return "Empty room";
2940+
}
2941+
}
2942+
}
2943+
29172944
/**
29182945
* This is an internal method. Calculates the name of the room from the current
29192946
* room state.
@@ -2928,14 +2955,20 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
29282955
// check for an alias, if any. for now, assume first alias is the
29292956
// official one.
29302957
const mRoomName = this.currentState.getStateEvents(EventType.RoomName, "");
2931-
if (mRoomName && mRoomName.getContent() && mRoomName.getContent().name) {
2932-
return mRoomName.getContent().name;
2958+
if (mRoomName?.getContent().name) {
2959+
return this.roomNameGenerator({
2960+
type: RoomNameType.Actual,
2961+
name: mRoomName.getContent().name,
2962+
});
29332963
}
29342964
}
29352965

29362966
const alias = this.getCanonicalAlias();
29372967
if (alias) {
2938-
return alias;
2968+
return this.roomNameGenerator({
2969+
type: RoomNameType.Actual,
2970+
name: alias,
2971+
});
29392972
}
29402973

29412974
const joinedMemberCount = this.currentState.getJoinedMemberCount();
@@ -2967,8 +3000,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
29673000
});
29683001
} else {
29693002
let otherMembers = this.currentState.getMembers().filter((m) => {
2970-
return m.userId !== userId &&
2971-
(m.membership === "invite" || m.membership === "join");
3003+
return m.userId !== userId && (m.membership === "invite" || m.membership === "join");
29723004
});
29733005
otherMembers = otherMembers.filter(({ userId }) => {
29743006
// filter service members
@@ -2986,24 +3018,33 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
29863018
}
29873019

29883020
if (inviteJoinCount) {
2989-
return memberNamesToRoomName(otherNames, inviteJoinCount);
3021+
return this.roomNameGenerator({
3022+
type: RoomNameType.Generated,
3023+
names: otherNames,
3024+
count: inviteJoinCount,
3025+
});
29903026
}
29913027

29923028
const myMembership = this.getMyMembership();
29933029
// if I have created a room and invited people through
29943030
// 3rd party invites
29953031
if (myMembership == 'join') {
2996-
const thirdPartyInvites =
2997-
this.currentState.getStateEvents(EventType.RoomThirdPartyInvite);
3032+
const thirdPartyInvites = this.currentState.getStateEvents(EventType.RoomThirdPartyInvite);
29983033

2999-
if (thirdPartyInvites && thirdPartyInvites.length) {
3034+
if (thirdPartyInvites?.length) {
30003035
const thirdPartyNames = thirdPartyInvites.map((i) => {
30013036
return i.getContent().display_name;
30023037
});
30033038

3004-
return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`;
3039+
return this.roomNameGenerator({
3040+
type: RoomNameType.Generated,
3041+
subtype: "Inviting",
3042+
names: thirdPartyNames,
3043+
count: thirdPartyNames.length + 1,
3044+
});
30053045
}
30063046
}
3047+
30073048
// let's try to figure out who was here before
30083049
let leftNames = otherNames;
30093050
// if we didn't have heroes, try finding them in the room state
@@ -3014,11 +3055,20 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
30143055
m.membership !== "join";
30153056
}).map((m) => m.name);
30163057
}
3058+
3059+
let oldName: string;
30173060
if (leftNames.length) {
3018-
return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
3019-
} else {
3020-
return "Empty room";
3061+
oldName = this.roomNameGenerator({
3062+
type: RoomNameType.Generated,
3063+
names: leftNames,
3064+
count: leftNames.length + 1,
3065+
});
30213066
}
3067+
3068+
return this.roomNameGenerator({
3069+
type: RoomNameType.EmptyRoom,
3070+
oldName,
3071+
});
30223072
}
30233073

30243074
/**
@@ -3203,8 +3253,33 @@ const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
32033253
[EventStatus.CANCELLED]: [],
32043254
};
32053255

3206-
// TODO i18n
3207-
function memberNamesToRoomName(names: string[], count = (names.length + 1)) {
3256+
export enum RoomNameType {
3257+
EmptyRoom,
3258+
Generated,
3259+
Actual,
3260+
}
3261+
3262+
export interface EmptyRoomNameState {
3263+
type: RoomNameType.EmptyRoom;
3264+
oldName?: string;
3265+
}
3266+
3267+
export interface GeneratedRoomNameState {
3268+
type: RoomNameType.Generated;
3269+
subtype?: "Inviting";
3270+
names: string[];
3271+
count: number;
3272+
}
3273+
3274+
export interface ActualRoomNameState {
3275+
type: RoomNameType.Actual;
3276+
name: string;
3277+
}
3278+
3279+
export type RoomNameState = EmptyRoomNameState | GeneratedRoomNameState | ActualRoomNameState;
3280+
3281+
// Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn
3282+
function memberNamesToRoomName(names: string[], count: number): string {
32083283
const countWithoutMe = count - 1;
32093284
if (!names.length) {
32103285
return "Empty room";

0 commit comments

Comments
 (0)