make power level api more reusable

This commit is contained in:
Ajay Bura 2024-05-01 10:04:15 +05:30
parent da026b4e12
commit e5330c5e13
6 changed files with 132 additions and 61 deletions

View file

@ -25,7 +25,7 @@ import { nameInitials } from '../../utils/common';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { usePowerLevels } from '../../hooks/usePowerLevels';
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom';
import { getOriginBaseUrl, withOriginBaseUrl } from '../../pages/pathUtils';
import { markAsRead } from '../../../client/action/notifications';
@ -42,7 +42,8 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
({ room, linkPath, requestClose }, ref) => {
const mx = useMatrixClient();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const { getPowerLevel, canDoAction } = usePowerLevels(room);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const handleMarkAsRead = () => {

View file

@ -22,10 +22,10 @@ export function Room() {
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const screenSize = useScreenSize();
const powerLevelAPI = usePowerLevels(room);
const powerLevels = usePowerLevels(room);
return (
<PowerLevelsContextProvider value={powerLevelAPI}>
<PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes">
<RoomView room={room} eventId={eventId} />
{screenSize === ScreenSize.Desktop && isDrawer && (

View file

@ -101,7 +101,7 @@ import * as css from './RoomTimeline.css';
import { inSameDay, minuteDifference, timeDayMonthYear, today, yesterday } from '../../utils/time';
import { createMentionElement, isEmptyEditor, moveCursor } from '../../components/editor';
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
import { usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
import initMatrix from '../../../client/initMatrix';
import { useKeyDown } from '../../hooks/useKeyDown';
@ -437,7 +437,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const showUrlPreview = encryptedRoom ? encUrlPreview : urlPreview;
const [showHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId));
const { canDoAction, canSendEvent, getPowerLevel } = usePowerLevelsAPI();
const powerLevels = usePowerLevelsContext();
const { canDoAction, canSendEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels);
const myPowerLevel = getPowerLevel(mx.getUserId() ?? '');
const canRedact = canDoAction('redact', myPowerLevel);
const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);

View file

@ -4,7 +4,7 @@ import { EventType, Room } from 'matrix-js-sdk';
import { useStateEvent } from '../../hooks/useStateEvent';
import { StateEvent } from '../../../types/matrix/room';
import { usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useEditor } from '../../components/editor';
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
@ -26,7 +26,8 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
const mx = useMatrixClient();
const tombstoneEvent = useStateEvent(room, StateEvent.RoomTombstone);
const { getPowerLevel, canSendEvent } = usePowerLevelsAPI();
const powerLevels = usePowerLevelsContext();
const { getPowerLevel, canSendEvent } = usePowerLevelsAPI(powerLevels);
const myUserId = mx.getUserId();
const canMessage = myUserId
? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId))

View file

@ -47,7 +47,7 @@ import { getCanonicalAliasOrRoomId } from '../../utils/matrix';
import { _SearchPathSearchParams } from '../../pages/paths';
import * as css from './RoomViewHeader.css';
import { useRoomUnread } from '../../state/hooks/unread';
import { usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { markAsRead } from '../../../client/action/notifications';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { openInviteUser, toggleRoomSettings } from '../../../client/action/navigation';
@ -65,7 +65,8 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
({ room, linkPath, requestClose }, ref) => {
const mx = useMatrixClient();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI();
const powerLevels = usePowerLevelsContext();
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const handleMarkAsRead = () => {

View file

@ -1,11 +1,15 @@
import { Room } from 'matrix-js-sdk';
import { createContext, useCallback, useContext } from 'react';
import { createContext, useCallback, useContext, useMemo } from 'react';
import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room';
import { useForceUpdate } from './useForceUpdate';
import { useStateEventCallback } from './useStateEventCallback';
import { useMatrixClient } from './useMatrixClient';
import { getStateEvent } from '../utils/room';
export type PowerLevelActions = 'invite' | 'redact' | 'kick' | 'ban' | 'historical';
enum DefaultPowerLevels {
export enum DefaultPowerLevels {
usersDefault = 0,
stateDefault = 50,
eventsDefault = 0,
@ -16,7 +20,7 @@ enum DefaultPowerLevels {
historical = 0,
}
interface IPowerLevels {
export interface IPowerLevels {
users_default?: number;
state_default?: number;
events_default?: number;
@ -31,9 +35,75 @@ interface IPowerLevels {
notifications?: Record<string, number>;
}
export type GetPowerLevel = (userId: string) => number;
export type CanSend = (eventType: string | undefined, powerLevel: number) => boolean;
export type CanDoAction = (action: PowerLevelActions, powerLevel: number) => boolean;
export function usePowerLevels(room: Room): IPowerLevels {
const powerLevelsEvent = useStateEvent(room, StateEvent.RoomPowerLevels);
const powerLevels: IPowerLevels =
powerLevelsEvent?.getContent<IPowerLevels>() ?? DefaultPowerLevels;
return powerLevels;
}
export const PowerLevelsContext = createContext<IPowerLevels | null>(null);
export const PowerLevelsContextProvider = PowerLevelsContext.Provider;
export const usePowerLevelsContext = (): IPowerLevels => {
const pl = useContext(PowerLevelsContext);
if (!pl) throw new Error('PowerLevelContext is not initialized!');
return pl;
};
export const useRoomsPowerLevels = (rooms: Room[]): Map<string, IPowerLevels> => {
const mx = useMatrixClient();
const [updateCount, forceUpdate] = useForceUpdate();
useStateEventCallback(
mx,
useCallback(
(event) => {
const roomId = event.getRoomId();
if (
roomId &&
event.getType() === StateEvent.RoomPowerLevels &&
event.getStateKey() === '' &&
rooms.find((r) => r.roomId === roomId)
) {
forceUpdate();
}
},
[rooms, forceUpdate]
)
);
const roomToPowerLevels = useMemo(
() => {
const rToPl = new Map<string, IPowerLevels>();
rooms.forEach((room) => {
const pl = getStateEvent(room, StateEvent.RoomPowerLevels, '')?.getContent<IPowerLevels>();
if (pl) rToPl.set(room.roomId, pl);
});
return rToPl;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[rooms, updateCount]
);
return roomToPowerLevels;
};
export type GetPowerLevel = (powerLevels: IPowerLevels, userId: string | undefined) => number;
export type CanSend = (
powerLevels: IPowerLevels,
eventType: string | undefined,
powerLevel: number
) => boolean;
export type CanDoAction = (
powerLevels: IPowerLevels,
action: PowerLevelActions,
powerLevel: number
) => boolean;
export type PowerLevelsAPI = {
getPowerLevel: GetPowerLevel;
@ -42,51 +112,58 @@ export type PowerLevelsAPI = {
canDoAction: CanDoAction;
};
export function usePowerLevels(room: Room): PowerLevelsAPI {
const powerLevelsEvent = useStateEvent(room, StateEvent.RoomPowerLevels);
const powerLevels: IPowerLevels = powerLevelsEvent?.getContent() ?? DefaultPowerLevels;
export const powerLevelAPI: PowerLevelsAPI = {
getPowerLevel: (powerLevels, userId) => {
const { users_default: usersDefault, users } = powerLevels;
if (userId && users && typeof users[userId] === 'number') {
return users[userId];
}
return usersDefault ?? DefaultPowerLevels.usersDefault;
},
canSendEvent: (powerLevels, eventType, powerLevel) => {
const { events, events_default: eventsDefault } = powerLevels;
if (events && eventType && typeof events[eventType] === 'number') {
return powerLevel >= events[eventType];
}
return powerLevel >= (eventsDefault ?? DefaultPowerLevels.eventsDefault);
},
canSendStateEvent: (powerLevels, eventType, powerLevel) => {
const { events, state_default: stateDefault } = powerLevels;
if (events && eventType && typeof events[eventType] === 'number') {
return powerLevel >= events[eventType];
}
return powerLevel >= (stateDefault ?? DefaultPowerLevels.stateDefault);
},
canDoAction: (powerLevels, action, powerLevel) => {
const requiredPL = powerLevels[action];
if (typeof requiredPL === 'number') {
return powerLevel >= requiredPL;
}
return powerLevel >= DefaultPowerLevels[action];
},
};
const getPowerLevel: GetPowerLevel = useCallback(
(userId) => {
const { users_default: usersDefault, users } = powerLevels;
if (users && typeof users[userId] === 'number') {
return users[userId];
}
return usersDefault ?? DefaultPowerLevels.usersDefault;
},
export const usePowerLevelsAPI = (powerLevels: IPowerLevels) => {
const getPowerLevel = useCallback(
(userId: string | undefined) => powerLevelAPI.getPowerLevel(powerLevels, userId),
[powerLevels]
);
const canSendEvent: CanSend = useCallback(
(eventType, powerLevel) => {
const { events, events_default: eventsDefault } = powerLevels;
if (events && eventType && typeof events[eventType] === 'number') {
return powerLevel >= events[eventType];
}
return powerLevel >= (eventsDefault ?? DefaultPowerLevels.eventsDefault);
},
const canSendEvent = useCallback(
(eventType: string | undefined, powerLevel: number) =>
powerLevelAPI.canSendEvent(powerLevels, eventType, powerLevel),
[powerLevels]
);
const canSendStateEvent: CanSend = useCallback(
(eventType, powerLevel) => {
const { events, state_default: stateDefault } = powerLevels;
if (events && eventType && typeof events[eventType] === 'number') {
return powerLevel >= events[eventType];
}
return powerLevel >= (stateDefault ?? DefaultPowerLevels.stateDefault);
},
const canSendStateEvent = useCallback(
(eventType: string | undefined, powerLevel: number) =>
powerLevelAPI.canSendStateEvent(powerLevels, eventType, powerLevel),
[powerLevels]
);
const canDoAction: CanDoAction = useCallback(
(action, powerLevel) => {
const requiredPL = powerLevels[action];
if (typeof requiredPL === 'number') {
return powerLevel >= requiredPL;
}
return powerLevel >= DefaultPowerLevels[action];
},
const canDoAction = useCallback(
(action: PowerLevelActions, powerLevel: number) =>
powerLevelAPI.canDoAction(powerLevels, action, powerLevel),
[powerLevels]
);
@ -96,14 +173,4 @@ export function usePowerLevels(room: Room): PowerLevelsAPI {
canSendStateEvent,
canDoAction,
};
}
export const PowerLevelsContext = createContext<PowerLevelsAPI | null>(null);
export const PowerLevelsContextProvider = PowerLevelsContext.Provider;
export const usePowerLevelsAPI = (): PowerLevelsAPI => {
const api = useContext(PowerLevelsContext);
if (!api) throw new Error('PowerLevelContext is not initialized!');
return api;
};