You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-12-04 05:02:41 +03:00
Support for creator power level (#4937)
* Support for creator power level Adds support for infinite power level specified by [MSC4289](https://github.com/matrix-org/matrix-spec-proposals/pull/4289). * Update unit test * Hardcode versions as room versions strings aren't ordered * Add test for v12 rooms * Use more compact syntax Co-authored-by: R Midhun Suresh <hi@midhun.dev> * Fix doc Co-authored-by: R Midhun Suresh <hi@midhun.dev> * Fix additionalCreators from PR edit * Split out hydra room version check * Move power level logic into room state Which already has knowledge of the room create event * Add docs * Fix unused bits * Fix docs * Fix lying docstring * Reverse logic for hydra semantics Assume unknown room versions do use hydra * Use backticks Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Switch back to hardcoding just the two hydra versions --------- Co-authored-by: R Midhun Suresh <hi@midhun.dev> Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
@@ -66,7 +66,6 @@ export type RoomMemberEventHandlerMap = {
|
||||
* ```
|
||||
* matrixClient.on("RoomMember.powerLevel", function(event, member){
|
||||
* var newPowerLevel = member.powerLevel;
|
||||
* var newNormPowerLevel = member.powerLevelNorm;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@@ -109,10 +108,6 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
* The power level for this room member.
|
||||
*/
|
||||
public powerLevel = 0;
|
||||
/**
|
||||
* The normalised power level (0-100) for this room member.
|
||||
*/
|
||||
public powerLevelNorm = 0;
|
||||
/**
|
||||
* The User object for this room member, if one exists.
|
||||
*/
|
||||
@@ -226,43 +221,18 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this room member's power level event. May fire
|
||||
* "RoomMember.powerLevel" if this event updates this member's power levels.
|
||||
* @param powerLevelEvent - The `m.room.power_levels` event
|
||||
* Update this room member's power level event. Will fire
|
||||
* "RoomMember.powerLevel" if the new power level is different
|
||||
* @param powerLevel - The power level of the room member.
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomMemberEvent.PowerLevel}
|
||||
*/
|
||||
public setPowerLevelEvent(powerLevelEvent: MatrixEvent): void {
|
||||
if (powerLevelEvent.getType() !== EventType.RoomPowerLevels || powerLevelEvent.getStateKey() !== "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const evContent = powerLevelEvent.getDirectionalContent();
|
||||
|
||||
let maxLevel = evContent.users_default || 0;
|
||||
const users: { [userId: string]: number } = evContent.users || {};
|
||||
Object.values(users).forEach((lvl: number) => {
|
||||
maxLevel = Math.max(maxLevel, lvl);
|
||||
});
|
||||
public setPowerLevel(powerLevel: number, powerLevelEvent: MatrixEvent): void {
|
||||
const oldPowerLevel = this.powerLevel;
|
||||
const oldPowerLevelNorm = this.powerLevelNorm;
|
||||
this.powerLevel = powerLevel;
|
||||
|
||||
if (users[this.userId] !== undefined && Number.isInteger(users[this.userId])) {
|
||||
this.powerLevel = users[this.userId];
|
||||
} else if (evContent.users_default !== undefined) {
|
||||
this.powerLevel = evContent.users_default;
|
||||
} else {
|
||||
this.powerLevel = 0;
|
||||
}
|
||||
this.powerLevelNorm = 0;
|
||||
if (maxLevel > 0) {
|
||||
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
|
||||
}
|
||||
|
||||
// emit for changes in powerLevelNorm as well (since the app will need to
|
||||
// redraw everyone's level if the max has changed)
|
||||
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
||||
if (oldPowerLevel !== this.powerLevel) {
|
||||
this.updateModifiedTime();
|
||||
this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { TypedReEmitter } from "../ReEmitter.ts";
|
||||
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon.ts";
|
||||
import { KnownMembership } from "../@types/membership.ts";
|
||||
import { type RoomJoinRulesEventContent } from "../@types/state_events.ts";
|
||||
import { shouldUseHydraForRoomVersion } from "../utils/roomVersion.ts";
|
||||
|
||||
export interface IMarkerFoundOptions {
|
||||
/** Whether the timeline was empty before the marker event arrived in the
|
||||
@@ -173,6 +174,9 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
private _liveBeaconIds: BeaconIdentifier[] = [];
|
||||
|
||||
// We only wants to print warnings about bad room state once.
|
||||
private getVersionWarning = false;
|
||||
|
||||
/**
|
||||
* Construct room state.
|
||||
*
|
||||
@@ -209,6 +213,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
this.updateModifiedTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of the room
|
||||
* @returns The version of the room
|
||||
*/
|
||||
public getRoomVersion(): string {
|
||||
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||
if (!createEvent) {
|
||||
if (!this.getVersionWarning) {
|
||||
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
|
||||
this.getVersionWarning = true;
|
||||
}
|
||||
return "1";
|
||||
}
|
||||
return createEvent.getContent()["room_version"] ?? "1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of joined members in this room
|
||||
* This method caches the result.
|
||||
@@ -468,12 +488,20 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
return;
|
||||
}
|
||||
const members = Object.values(this.members);
|
||||
|
||||
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||
const creators = getCreators(this.getRoomVersion(), createEvent);
|
||||
|
||||
members.forEach((member) => {
|
||||
// We only propagate `RoomState.members` event if the
|
||||
// power levels has been changed
|
||||
// large room suffer from large re-rendering especially when not needed
|
||||
const oldLastModified = member.getLastModifiedTime();
|
||||
member.setPowerLevelEvent(event);
|
||||
|
||||
if (createEvent) {
|
||||
const pl = powerLevelForUserId(member.userId, event, creators);
|
||||
member.setPowerLevel(pl, event);
|
||||
}
|
||||
if (oldLastModified !== member.getLastModifiedTime()) {
|
||||
this.emit(RoomStateEvent.Members, event, this, member);
|
||||
}
|
||||
@@ -625,9 +653,16 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
|
||||
private updateMember(member: RoomMember): void {
|
||||
// this member may have a power level already, so set it.
|
||||
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||
const pwrLvlEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (pwrLvlEvent) {
|
||||
member.setPowerLevelEvent(pwrLvlEvent);
|
||||
if (pwrLvlEvent && createEvent) {
|
||||
const powerLevel = powerLevelForUserId(
|
||||
member.userId,
|
||||
pwrLvlEvent,
|
||||
getCreators(this.getRoomVersion(), createEvent),
|
||||
);
|
||||
|
||||
member.setPowerLevel(powerLevel, pwrLvlEvent);
|
||||
}
|
||||
|
||||
// blow away the sentinel which is now outdated
|
||||
@@ -1106,3 +1141,46 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of creator user IDs for a room: empty if the room is not a 'hydra' room, otherwise
|
||||
* computed from the sender of the m.room.create event plus the additional_creators field.
|
||||
* @param roomVersion The version of the room
|
||||
* @param roomCreateEvent The m.room.create event for the room
|
||||
* @returns A set of user IDs of the creators of the room.
|
||||
*/
|
||||
function getCreators(roomVersion: string, roomCreateEvent: MatrixEvent | null): Set<string> {
|
||||
const creators = new Set<string>();
|
||||
if (shouldUseHydraForRoomVersion(roomVersion) && roomCreateEvent) {
|
||||
const roomCreateSender = roomCreateEvent.getSender();
|
||||
if (roomCreateSender) creators.add(roomCreateSender);
|
||||
const additionalCreators = roomCreateEvent.getDirectionalContent().additional_creators;
|
||||
if (Array.isArray(additionalCreators)) additionalCreators.forEach((c) => creators.add(c));
|
||||
}
|
||||
return creators;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userId The user ID to compute the power level for
|
||||
* @param powerLevelEvents The power level event for the room
|
||||
* @param creators The set of creator user IDs for the room if the room is a 'hydra' room, otherwise the empty set.
|
||||
*/
|
||||
function powerLevelForUserId(userId: string, powerLevelEvent: MatrixEvent, creators: Set<string>): number {
|
||||
if (creators.has(userId)) {
|
||||
// As of "Hydra", If the user is a creator, they always have the highest power level
|
||||
return Infinity;
|
||||
} else {
|
||||
const evContent = powerLevelEvent.getDirectionalContent();
|
||||
|
||||
const users: { [userId: string]: number } = evContent.users || {};
|
||||
|
||||
if (users[userId] !== undefined && Number.isInteger(users[userId])) {
|
||||
return users[userId];
|
||||
} else if (evContent.users_default !== undefined) {
|
||||
return evContent.users_default;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +374,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
private heroes: Hero[] | null = null;
|
||||
// flags to stop logspam about missing m.room.create events
|
||||
private getTypeWarning = false;
|
||||
private getVersionWarning = false;
|
||||
private membersPromise?: Promise<boolean>;
|
||||
|
||||
// XXX: These should be read-only
|
||||
@@ -606,18 +605,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
/**
|
||||
* Gets the version of the room
|
||||
* @returns The version of the room, or null if it could not be determined
|
||||
* @returns The version of the room
|
||||
*/
|
||||
public getVersion(): string {
|
||||
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (!createEvent) {
|
||||
if (!this.getVersionWarning) {
|
||||
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
|
||||
this.getVersionWarning = true;
|
||||
}
|
||||
return "1";
|
||||
}
|
||||
return createEvent.getContent()["room_version"] ?? "1";
|
||||
return this.currentState.getRoomVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
40
src/utils/roomVersion.ts
Normal file
40
src/utils/roomVersion.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Room versions strings that we know about and do not use hydra semantics.
|
||||
*/
|
||||
const HYDRA_ROOM_VERSIONS = ["org.matrix.hydra.11", "12"];
|
||||
|
||||
/**
|
||||
* Checks if the given room version is one where new "hydra" power level
|
||||
* semantics (ie. room version 12 or later) should be used
|
||||
* (see https://github.com/matrix-org/matrix-spec-proposals/pull/4289).
|
||||
* This will return `true` for versions that are known to the js-sdk and
|
||||
* use hydra: any room versions unknown to the js-sdk (experimental or
|
||||
* otherwise) will cause the function to return `false`.
|
||||
*
|
||||
* @param roomVersion - The version of the room to check.
|
||||
* @returns `true` if hydra semantics should be used for the room version, `false` otherwise.
|
||||
*/
|
||||
export function shouldUseHydraForRoomVersion(roomVersion: string): boolean {
|
||||
// Future new room versions must obviously be added to the constant above,
|
||||
// otherwise the js-sdk will use the old, pre-hydra semantics. At some point
|
||||
// it would make sense to assume hydra for unknown versions but this will break
|
||||
// any rooms using unknown versions, so at hydra switch time we've agreed all
|
||||
// Element clients will only use hydra for the two specific hydra versions.
|
||||
return HYDRA_ROOM_VERSIONS.includes(roomVersion);
|
||||
}
|
||||
Reference in New Issue
Block a user