diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index 18ca2687..8bcd5fa1 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -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( ({ 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 = () => { diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx index 261b2fb1..fbe16a65 100644 --- a/src/app/features/room/Room.tsx +++ b/src/app/features/room/Room.tsx @@ -22,10 +22,10 @@ export function Room() { const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const screenSize = useScreenSize(); - const powerLevelAPI = usePowerLevels(room); + const powerLevels = usePowerLevels(room); return ( - + {screenSize === ScreenSize.Desktop && isDrawer && ( diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 479dab15..29b874fc 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -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); diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx index 70612474..fe145b37 100644 --- a/src/app/features/room/RoomView.tsx +++ b/src/app/features/room/RoomView.tsx @@ -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)) diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index ca3b68c0..f1696ca7 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -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( ({ 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 = () => { diff --git a/src/app/hooks/usePowerLevels.ts b/src/app/hooks/usePowerLevels.ts index 98e3701e..3814d40b 100644 --- a/src/app/hooks/usePowerLevels.ts +++ b/src/app/hooks/usePowerLevels.ts @@ -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; } -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() ?? DefaultPowerLevels; + + return powerLevels; +} + +export const PowerLevelsContext = createContext(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 => { + 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(); + + rooms.forEach((room) => { + const pl = getStateEvent(room, StateEvent.RoomPowerLevels, '')?.getContent(); + 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(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; };