Support room version 12 (#2399)
Some checks are pending
Deploy to Netlify (dev) / Deploy to Netlify (push) Waiting to run

* WIP - support room version 12

* add room creators hook

* revert changes from powerlevels

* improve use room creators hook

* add hook to get dm users

* add options to add creators in create room/space

* add member item component in member drawer

* remove unused import

* extract member drawer header component

* get room creators as set only if room version support them

* add room permissions hook

* support room v12 creators power

* make predecessor event id optional

* add info about founders in permissions

* allow to create infinite powers to room creators

* allow everyone with permission to create infinite power

* handle additional creators in room upgrade

* add option to follow space tombstone
This commit is contained in:
Ajay Bura 2025-08-12 19:42:30 +05:30 committed by GitHub
parent 4d1ae4eafd
commit f82cfead46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 1717 additions and 783 deletions

View file

@ -27,6 +27,9 @@ import { stopPropagation } from '../../utils/keyboard';
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
import { IPowerLevels } from '../../hooks/usePowerLevels';
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
type HierarchyItemWithParent = HierarchyItem & {
parentId: string;
@ -45,7 +48,7 @@ function SuggestMenuItem({
const [toggleState, handleToggleSuggested] = useAsyncCallback(
useCallback(() => {
const newContent: MSpaceChildContent = { ...content, suggested: !content.suggested };
return mx.sendStateEvent(parentId, StateEvent.SpaceChild, newContent, roomId);
return mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, newContent, roomId);
}, [mx, parentId, roomId, content])
);
@ -82,7 +85,7 @@ function RemoveMenuItem({
const [removeState, handleRemove] = useAsyncCallback(
useCallback(
() => mx.sendStateEvent(parentId, StateEvent.SpaceChild, {}, roomId),
() => mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, {}, roomId),
[mx, parentId, roomId]
)
);
@ -180,7 +183,7 @@ type HierarchyItemMenuProps = {
parentId: string;
};
joined: boolean;
canInvite: boolean;
powerLevels?: IPowerLevels;
canEditChild: boolean;
pinned?: boolean;
onTogglePin?: (roomId: string) => void;
@ -188,13 +191,22 @@ type HierarchyItemMenuProps = {
export function HierarchyItemMenu({
item,
joined,
canInvite,
powerLevels,
canEditChild,
pinned,
onTogglePin,
}: HierarchyItemMenuProps) {
const mx = useMatrixClient();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const canInvite = (): boolean => {
if (!powerLevels) return false;
const creators = getRoomCreatorsForRoomId(mx, item.roomId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
return permissions.action('invite', mx.getSafeUserId());
};
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
@ -254,7 +266,7 @@ export function HierarchyItemMenu({
<InviteMenuItem
item={item}
requestClose={handleRequestClose}
disabled={!canInvite}
disabled={!canInvite()}
/>
<SettingsMenuItem item={item} requestClose={handleRequestClose} />
<UseStateProvider initial={false}>

View file

@ -27,7 +27,6 @@ import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
import {
IPowerLevels,
PowerLevelsContextProvider,
powerLevelAPI,
usePowerLevels,
useRoomsPowerLevels,
} from '../../hooks/usePowerLevels';
@ -55,12 +54,13 @@ import { useRoomMembers } from '../../hooks/useRoomMembers';
import { SpaceHierarchy } from './SpaceHierarchy';
import { useGetRoom } from '../../hooks/useGetRoom';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
const useCanDropLobbyItem = (
space: Room,
roomsPowerLevels: Map<string, IPowerLevels>,
getRoom: (roomId: string) => Room | undefined,
canEditSpaceChild: (powerLevels: IPowerLevels) => boolean
getRoom: (roomId: string) => Room | undefined
): CanDropCallback => {
const mx = useMatrixClient();
@ -74,16 +74,20 @@ const useCanDropLobbyItem = (
const containerSpaceId = space.roomId;
const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {};
const creators = getRoomCreatorsForRoomId(mx, containerSpaceId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
if (
getRoom(containerSpaceId) === undefined ||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
!permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
) {
return false;
}
return true;
},
[space, roomsPowerLevels, getRoom, canEditSpaceChild]
[space, roomsPowerLevels, getRoom, mx]
);
const canDropRoom: CanDropCallback = useCallback(
@ -97,30 +101,31 @@ const useCanDropLobbyItem = (
// check and do not allow restricted room to be dragged outside
// current space if can't change `m.room.join_rules` `content.allow`
if (draggingOutsideSpace && restrictedItem) {
const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
const userPLInItem = powerLevelAPI.getPowerLevel(
itemPowerLevel,
mx.getUserId() ?? undefined
);
const canChangeJoinRuleAllow = powerLevelAPI.canSendStateEvent(
itemPowerLevel,
const itemPowerLevels = roomsPowerLevels.get(item.roomId) ?? {};
const itemCreators = getRoomCreatorsForRoomId(mx, item.roomId);
const itemPermissions = getRoomPermissionsAPI(itemCreators, itemPowerLevels);
const canChangeJoinRuleAllow = itemPermissions.stateEvent(
StateEvent.RoomJoinRules,
userPLInItem
mx.getSafeUserId()
);
if (!canChangeJoinRuleAllow) {
return false;
}
}
const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {};
const creators = getRoomCreatorsForRoomId(mx, containerSpaceId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
if (
getRoom(containerSpaceId) === undefined ||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
!permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
) {
return false;
}
return true;
},
[mx, getRoom, canEditSpaceChild, roomsPowerLevels]
[mx, getRoom, roomsPowerLevels]
);
const canDrop: CanDropCallback = useCallback(
@ -183,16 +188,6 @@ export function Lobby() {
const getRoom = useGetRoom(allJoinedRooms);
const canEditSpaceChild = useCallback(
(powerLevels: IPowerLevels) =>
powerLevelAPI.canSendStateEvent(
powerLevels,
StateEvent.SpaceChild,
powerLevelAPI.getPowerLevel(powerLevels, mx.getUserId() ?? undefined)
),
[mx]
);
const [draggingItem, setDraggingItem] = useState<HierarchyItem>();
const hierarchy = useSpaceHierarchy(
space.roomId,
@ -229,12 +224,7 @@ export function Lobby() {
)
);
const canDrop: CanDropCallback = useCanDropLobbyItem(
space,
roomsPowerLevels,
getRoom,
canEditSpaceChild
);
const canDrop: CanDropCallback = useCanDropLobbyItem(space, roomsPowerLevels, getRoom);
const [reorderSpaceState, reorderSpace] = useAsyncCallback(
useCallback(
@ -270,7 +260,11 @@ export function Lobby() {
.filter((reorder, index) => {
if (!reorder.item.parentId) return false;
const parentPL = roomsPowerLevels.get(reorder.item.parentId);
const canEdit = parentPL && canEditSpaceChild(parentPL);
if (!parentPL) return false;
const creators = getRoomCreatorsForRoomId(mx, reorder.item.parentId);
const permissions = getRoomPermissionsAPI(creators, parentPL);
const canEdit = permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId());
return canEdit && reorder.orderKey !== currentOrders[index];
});
@ -286,7 +280,7 @@ export function Lobby() {
});
}
},
[mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild]
[mx, hierarchy, lex, roomsPowerLevels]
)
);
const reorderingSpace = reorderSpaceState.status === AsyncStatus.Loading;
@ -428,7 +422,7 @@ export function Lobby() {
newItems.push(rId);
}
const newSpacesContent = makeCinnySpacesContent(mx, newItems);
mx.setAccountData(AccountDataEvent.CinnySpaces, newSpacesContent);
mx.setAccountData(AccountDataEvent.CinnySpaces as any, newSpacesContent as any);
},
[mx, sidebarItems, sidebarSpaces]
);
@ -493,7 +487,6 @@ export function Lobby() {
allJoinedRooms={allJoinedRooms}
mDirects={mDirects}
roomsPowerLevels={roomsPowerLevels}
canEditSpaceChild={canEditSpaceChild}
categoryId={categoryId}
closed={
closedCategories.has(categoryId) ||

View file

@ -27,7 +27,7 @@ import { RoomAvatar } from '../../components/room-avatar';
import { nameInitials } from '../../utils/common';
import * as css from './LobbyHeader.css';
import { openInviteUser } from '../../../client/action/navigation';
import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { IPowerLevels } from '../../hooks/usePowerLevels';
import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
import { stopPropagation } from '../../utils/keyboard';
@ -36,26 +36,30 @@ import { BackRouteHandler } from '../../components/BackRouteHandler';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
type LobbyMenuProps = {
roomId: string;
powerLevels: IPowerLevels;
requestClose: () => void;
};
const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
({ roomId, powerLevels, requestClose }, ref) => {
({ powerLevels, requestClose }, ref) => {
const mx = useMatrixClient();
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const space = useSpace();
const creators = useRoomCreators(space);
const permissions = useRoomPermissions(creators, powerLevels);
const canInvite = permissions.action('invite', mx.getSafeUserId());
const openSpaceSettings = useOpenSpaceSettings();
const handleInvite = () => {
openInviteUser(roomId);
openInviteUser(space.roomId);
requestClose();
};
const handleRoomSettings = () => {
openSpaceSettings(roomId);
openSpaceSettings(space.roomId);
requestClose();
};
@ -106,7 +110,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
</MenuItem>
{promptLeave && (
<LeaveSpacePrompt
roomId={roomId}
roomId={space.roomId}
onDone={requestClose}
onCancel={() => setPromptLeave(false)}
/>
@ -242,7 +246,6 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
}}
>
<LobbyMenu
roomId={space.roomId}
powerLevels={powerLevels}
requestClose={() => setMenuAnchor(undefined)}
/>

View file

@ -8,14 +8,16 @@ import {
HierarchyItemSpace,
useFetchSpaceHierarchyLevel,
} from '../../hooks/useSpaceHierarchy';
import { IPowerLevels, powerLevelAPI } from '../../hooks/usePowerLevels';
import { IPowerLevels } from '../../hooks/usePowerLevels';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { SpaceItemCard } from './SpaceItem';
import { AfterItemDropTarget, CanDropCallback } from './DnD';
import { HierarchyItemMenu } from './HierarchyItemMenu';
import { RoomItemCard } from './RoomItem';
import { RoomType } from '../../../types/matrix/room';
import { RoomType, StateEvent } from '../../../types/matrix/room';
import { SequenceCard } from '../../components/sequence-card';
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
type SpaceHierarchyProps = {
summary: IHierarchyRoom | undefined;
@ -24,7 +26,6 @@ type SpaceHierarchyProps = {
allJoinedRooms: Set<string>;
mDirects: Set<string>;
roomsPowerLevels: Map<string, IPowerLevels>;
canEditSpaceChild: (powerLevels: IPowerLevels) => boolean;
categoryId: string;
closed: boolean;
handleClose: MouseEventHandler<HTMLButtonElement>;
@ -48,7 +49,6 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
allJoinedRooms,
mDirects,
roomsPowerLevels,
canEditSpaceChild,
categoryId,
closed,
handleClose,
@ -79,25 +79,28 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
return s;
}, [rooms]);
const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId) ?? {};
const userPLInSpace = powerLevelAPI.getPowerLevel(
spacePowerLevels,
mx.getUserId() ?? undefined
);
const canInviteInSpace = powerLevelAPI.canDoAction(spacePowerLevels, 'invite', userPLInSpace);
const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId);
const spaceCreators = getRoomCreatorsForRoomId(mx, spaceItem.roomId);
const spacePermissions =
spacePowerLevels && getRoomPermissionsAPI(spaceCreators, spacePowerLevels);
const draggingSpace =
draggingItem?.roomId === spaceItem.roomId && draggingItem.parentId === spaceItem.parentId;
const { parentId } = spaceItem;
const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) ?? {} : undefined;
const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) : undefined;
const parentCreators = parentId ? getRoomCreatorsForRoomId(mx, parentId) : undefined;
const parentPermissions =
parentCreators &&
parentPowerLevels &&
getRoomPermissionsAPI(parentCreators, parentPowerLevels);
useEffect(() => {
onSpacesFound(Array.from(subspaces.values()));
}, [subspaces, onSpacesFound]);
let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId));
if (!canEditSpaceChild(spacePowerLevels)) {
if (!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())) {
// hide unknown rooms for normal user
childItems = childItems?.filter((i) => {
const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false;
@ -117,18 +120,22 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
closed={closed}
handleClose={handleClose}
getRoom={getRoom}
canEditChild={canEditSpaceChild(spacePowerLevels)}
canEditChild={!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())}
canReorder={
parentPowerLevels && !disabledReorder ? canEditSpaceChild(parentPowerLevels) : false
parentPowerLevels && !disabledReorder && parentPermissions
? parentPermissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
: false
}
options={
parentId &&
parentPowerLevels && (
<HierarchyItemMenu
item={{ ...spaceItem, parentId }}
canInvite={canInviteInSpace}
powerLevels={spacePowerLevels}
joined={allJoinedRooms.has(spaceItem.roomId)}
canEditChild={canEditSpaceChild(parentPowerLevels)}
canEditChild={
!!parentPermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
}
pinned={pinned}
onTogglePin={togglePinToSidebar}
/>
@ -151,15 +158,6 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
const roomSummary = rooms.get(roomItem.roomId);
const roomPowerLevels = roomsPowerLevels.get(roomItem.roomId) ?? {};
const userPLInRoom = powerLevelAPI.getPowerLevel(
roomPowerLevels,
mx.getUserId() ?? undefined
);
const canInviteInRoom = powerLevelAPI.canDoAction(
roomPowerLevels,
'invite',
userPLInRoom
);
const lastItem = index === childItems.length;
const nextRoomId = lastItem ? nextSpaceId : childItems[index + 1]?.roomId;
@ -178,13 +176,18 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
dm={mDirects.has(roomItem.roomId)}
onOpen={onOpenRoom}
getRoom={getRoom}
canReorder={canEditSpaceChild(spacePowerLevels) && !disabledReorder}
canReorder={
!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) &&
!disabledReorder
}
options={
<HierarchyItemMenu
item={roomItem}
canInvite={canInviteInRoom}
powerLevels={roomPowerLevels}
joined={allJoinedRooms.has(roomItem.roomId)}
canEditChild={canEditSpaceChild(spacePowerLevels)}
canEditChild={
!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
}
/>
}
after={