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

@ -84,16 +84,19 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import {
getTagIconSrc,
useAccessibleTagColors,
usePowerLevelTags,
} from '../../../hooks/usePowerLevelTags';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { useTheme } from '../../../hooks/useTheme';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
import { mDirectAtom } from '../../../state/mDirectList';
import {
getPowerTagIconSrc,
useAccessiblePowerTagColors,
useGetMemberPowerTag,
} from '../../../hooks/useMemberPowerTag';
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
type RoomNotificationsGroup = {
roomId: string;
@ -224,10 +227,14 @@ function RoomNotificationsGroupComp({
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
const creators = useRoomCreators(room);
const creatorsTag = useRoomCreatorsTag();
const powerLevelTags = usePowerLevelTags(room, powerLevels);
const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
const theme = useTheme();
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
@ -447,13 +454,12 @@ function RoomNotificationsGroupComp({
const threadRootId =
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
const senderPowerLevel = getPowerLevel(event.sender);
const powerLevelTag = getPowerLevelTag(senderPowerLevel);
const tagColor = powerLevelTag?.color
? accessibleTagColors?.get(powerLevelTag.color)
const memberPowerTag = getMemberPowerTag(event.sender);
const tagColor = memberPowerTag?.color
? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
const tagIconSrc = powerLevelTag?.icon
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
const tagIconSrc = memberPowerTag?.icon
? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
@ -523,8 +529,7 @@ function RoomNotificationsGroupComp({
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenClick}
getPowerLevel={getPowerLevel}
getPowerLevelTag={getPowerLevelTag}
getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor}
/>

View file

@ -77,7 +77,7 @@ import { AccountDataEvent } from '../../../../types/matrix/accountData';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useRoomsUnread } from '../../../state/hooks/unread';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { markAsRead } from '../../../../client/action/notifications';
@ -91,6 +91,8 @@ import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type SpaceMenuProps = {
room: Room;
@ -103,8 +105,10 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const canInvite = permissions.action('invite', mx.getSafeUserId());
const openSpaceSettings = useOpenSpaceSettings();
const allChild = useSpaceChildren(

View file

@ -10,6 +10,7 @@ import { useAtom, useAtomValue } from 'jotai';
import {
Avatar,
Box,
Button,
Icon,
IconButton,
Icons,
@ -18,7 +19,9 @@ import {
MenuItem,
PopOut,
RectCords,
Spinner,
Text,
color,
config,
toRem,
} from 'folds';
@ -53,7 +56,7 @@ import { useRoomName } from '../../../hooks/useRoomMeta';
import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { openInviteUser } from '../../../../client/action/navigation';
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
@ -64,7 +67,7 @@ import { LeaveSpacePrompt } from '../../../components/leave-space-prompt';
import { copyToClipboard } from '../../../utils/dom';
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
import { useStateEvent } from '../../../hooks/useStateEvent';
import { StateEvent } from '../../../../types/matrix/room';
import { Membership, StateEvent } from '../../../../types/matrix/room';
import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoom } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers';
@ -76,6 +79,11 @@ import {
} from '../../../hooks/useRoomsNotificationPreferences';
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
import { ContainerColor } from '../../../styles/ContainerColor.css';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { BreakWord } from '../../../styles/Text.css';
type SpaceMenuProps = {
room: Room;
@ -87,8 +95,10 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
const [developerTools] = useSetting(settingsAtom, 'developerTools');
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const canInvite = permissions.action('invite', mx.getSafeUserId());
const openSpaceSettings = useOpenSpaceSettings();
const { navigateRoom } = useRoomNavigate();
@ -284,6 +294,75 @@ function SpaceHeader() {
);
}
type SpaceTombstoneProps = { roomId: string; replacementRoomId: string };
export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProps) {
const mx = useMatrixClient();
const { navigateRoom } = useRoomNavigate();
const [joinState, handleJoin] = useAsyncCallback(
useCallback(() => {
const currentRoom = mx.getRoom(roomId);
const via = currentRoom ? getViaServers(currentRoom) : [];
return mx.joinRoom(replacementRoomId, {
viaServers: via,
});
}, [mx, roomId, replacementRoomId])
);
const replacementRoom = mx.getRoom(replacementRoomId);
const handleOpen = () => {
if (replacementRoom) navigateRoom(replacementRoom.roomId);
if (joinState.status === AsyncStatus.Success) navigateRoom(joinState.data.roomId);
};
return (
<Box
style={{
padding: config.space.S200,
borderRadius: config.radii.R400,
borderWidth: config.borderWidth.B300,
}}
className={ContainerColor({ variant: 'Surface' })}
direction="Column"
gap="300"
>
<Box direction="Column" grow="Yes" gap="100">
<Text size="L400">Space Upgraded</Text>
<Text size="T200">This space has been replaced and is no longer active.</Text>
{joinState.status === AsyncStatus.Error && (
<Text className={BreakWord} style={{ color: color.Critical.Main }} size="T200">
{(joinState.error as any)?.message ?? 'Failed to join replacement space!'}
</Text>
)}
</Box>
<Box direction="Column" shrink="No">
{replacementRoom?.getMyMembership() === Membership.Join ||
joinState.status === AsyncStatus.Success ? (
<Button onClick={handleOpen} size="300" variant="Success" fill="Solid" radii="300">
<Text size="B300">Open New Space</Text>
</Button>
) : (
<Button
onClick={handleJoin}
size="300"
variant="Primary"
fill="Solid"
radii="300"
before={
joinState.status === AsyncStatus.Loading && (
<Spinner size="100" variant="Primary" fill="Solid" />
)
}
disabled={joinState.status === AsyncStatus.Loading}
>
<Text size="B300">Join New Space</Text>
</Button>
)}
</Box>
</Box>
);
}
export function Space() {
const mx = useMatrixClient();
const space = useSpace();
@ -296,6 +375,8 @@ export function Space() {
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
const notificationPreferences = useRoomsNotificationPreferencesContext();
const tombstoneEvent = useStateEvent(space, StateEvent.RoomTombstone);
const selectedRoomId = useSelectedRoom();
const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias);
const searchSelected = useSpaceSearchSelected(spaceIdOrAlias);
@ -351,6 +432,12 @@ export function Space() {
<SpaceHeader />
<PageNavContent scrollRef={scrollRef}>
<Box direction="Column" gap="300">
{tombstoneEvent && (
<SpaceTombstone
roomId={space.roomId}
replacementRoomId={tombstoneEvent.getContent().replacement_room}
/>
)}
<NavCategory>
<NavItem variant="Background" radii="400" aria-selected={lobbySelected}>
<NavLink to={getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, space.roomId))}>