From f82cfead46db36a4d4b62af426499333ff3807aa Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:42:30 +0530 Subject: [PATCH] Support room version 12 (#2399) * 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 --- .../create-room/AdditionalCreatorInput.tsx | 306 ++++++++++++++++++ .../create-room/RoomVersionSelector.tsx | 2 +- src/app/components/create-room/index.ts | 1 + src/app/components/create-room/utils.ts | 13 +- .../image-pack-view/RoomImagePack.tsx | 9 +- src/app/components/message/Reply.tsx | 11 +- src/app/components/room-intro/RoomIntro.tsx | 2 +- .../components/user-profile/CreatorChip.tsx | 101 ++++++ src/app/components/user-profile/PowerChip.tsx | 47 ++- .../user-profile/UserRoomProfile.tsx | 38 ++- .../developer-tools/StateEventEditor.tsx | 10 +- .../emojis-stickers/RoomPacks.tsx | 10 +- .../common-settings/general/RoomAddress.tsx | 22 +- .../general/RoomEncryption.tsx | 14 +- .../general/RoomHistoryVisibility.tsx | 14 +- .../common-settings/general/RoomJoinRules.tsx | 13 +- .../common-settings/general/RoomProfile.tsx | 14 +- .../common-settings/general/RoomPublish.tsx | 13 +- .../common-settings/general/RoomUpgrade.tsx | 242 ++++++++------ .../common-settings/members/Members.tsx | 25 +- .../permissions/PermissionGroups.tsx | 31 +- .../common-settings/permissions/Powers.tsx | 42 ++- .../permissions/PowersEditor.tsx | 29 +- src/app/features/create-room/CreateRoom.tsx | 33 +- src/app/features/create-space/CreateSpace.tsx | 34 +- src/app/features/lobby/HierarchyItemMenu.tsx | 22 +- src/app/features/lobby/Lobby.tsx | 63 ++-- src/app/features/lobby/LobbyHeader.tsx | 21 +- src/app/features/lobby/SpaceHierarchy.tsx | 59 ++-- .../message-search/SearchResultGroup.tsx | 39 ++- src/app/features/room-nav/RoomNavItem.tsx | 10 +- .../room-settings/general/General.tsx | 20 +- .../room-settings/permissions/Permissions.tsx | 21 +- src/app/features/room/MembersDrawer.css.ts | 4 +- src/app/features/room/MembersDrawer.tsx | 237 ++++++++------ src/app/features/room/RoomInput.tsx | 31 +- src/app/features/room/RoomTimeline.tsx | 70 ++-- src/app/features/room/RoomView.tsx | 21 +- src/app/features/room/RoomViewHeader.tsx | 10 +- src/app/features/room/message/Message.tsx | 18 +- .../room/room-pin-menu/RoomPinMenu.tsx | 91 ++++-- .../space-settings/general/General.tsx | 16 +- .../permissions/Permissions.tsx | 21 +- src/app/hooks/useDirectUsers.ts | 27 ++ src/app/hooks/useMemberPowerCompare.ts | 28 ++ src/app/hooks/useMemberPowerTag.ts | 87 +++++ src/app/hooks/useMemberSort.ts | 19 +- src/app/hooks/usePowerLevelTags.ts | 115 ++----- src/app/hooks/usePowerLevels.ts | 93 +----- src/app/hooks/useRoomCreators.ts | 49 +++ src/app/hooks/useRoomCreatorsTag.ts | 8 + src/app/hooks/useRoomPermissions.ts | 60 ++++ src/app/pages/client/inbox/Notifications.tsx | 39 ++- src/app/pages/client/sidebar/SpaceTabs.tsx | 10 +- src/app/pages/client/space/Space.tsx | 95 +++++- src/app/utils/matrix.ts | 4 + src/types/matrix/accountData.ts | 2 + src/types/matrix/room.ts | 14 +- 58 files changed, 1717 insertions(+), 783 deletions(-) create mode 100644 src/app/components/create-room/AdditionalCreatorInput.tsx create mode 100644 src/app/components/user-profile/CreatorChip.tsx create mode 100644 src/app/hooks/useDirectUsers.ts create mode 100644 src/app/hooks/useMemberPowerCompare.ts create mode 100644 src/app/hooks/useMemberPowerTag.ts create mode 100644 src/app/hooks/useRoomCreators.ts create mode 100644 src/app/hooks/useRoomCreatorsTag.ts create mode 100644 src/app/hooks/useRoomPermissions.ts diff --git a/src/app/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx new file mode 100644 index 00000000..51334b49 --- /dev/null +++ b/src/app/components/create-room/AdditionalCreatorInput.tsx @@ -0,0 +1,306 @@ +import { + Box, + Button, + Chip, + config, + Icon, + Icons, + Input, + Line, + Menu, + MenuItem, + PopOut, + RectCords, + Scroll, + Text, + toRem, +} from 'folds'; +import { isKeyHotkey } from 'is-hotkey'; +import FocusTrap from 'focus-trap-react'; +import React, { + ChangeEventHandler, + KeyboardEventHandler, + MouseEventHandler, + useMemo, + useState, +} from 'react'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; +import { useDirectUsers } from '../../hooks/useDirectUsers'; +import { SettingTile } from '../setting-tile'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { stopPropagation } from '../../utils/keyboard'; +import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; +import { findAndReplace } from '../../utils/findAndReplace'; +import { highlightText } from '../../styles/CustomHtml.css'; +import { makeHighlightRegex } from '../../plugins/react-custom-html-parser'; + +export const useAdditionalCreators = (defaultCreators?: string[]) => { + const mx = useMatrixClient(); + const [additionalCreators, setAdditionalCreators] = useState( + () => defaultCreators?.filter((id) => id !== mx.getSafeUserId()) ?? [] + ); + + const addAdditionalCreator = (userId: string) => { + if (userId === mx.getSafeUserId()) return; + + setAdditionalCreators((creators) => { + const creatorsSet = new Set(creators); + creatorsSet.add(userId); + return Array.from(creatorsSet); + }); + }; + + const removeAdditionalCreator = (userId: string) => { + setAdditionalCreators((creators) => { + const creatorsSet = new Set(creators); + creatorsSet.delete(userId); + return Array.from(creatorsSet); + }); + }; + + return { + additionalCreators, + addAdditionalCreator, + removeAdditionalCreator, + }; +}; + +const SEARCH_OPTIONS: UseAsyncSearchOptions = { + limit: 1000, + matchOptions: { + contain: true, + }, +}; +const getUserIdString = (userId: string) => getMxIdLocalPart(userId) ?? userId; + +type AdditionalCreatorInputProps = { + additionalCreators: string[]; + onSelect: (userId: string) => void; + onRemove: (userId: string) => void; + disabled?: boolean; +}; +export function AdditionalCreatorInput({ + additionalCreators, + onSelect, + onRemove, + disabled, +}: AdditionalCreatorInputProps) { + const mx = useMatrixClient(); + const [menuCords, setMenuCords] = useState(); + const directUsers = useDirectUsers(); + + const [validUserId, setValidUserId] = useState(); + const filteredUsers = useMemo( + () => directUsers.filter((userId) => !additionalCreators.includes(userId)), + [directUsers, additionalCreators] + ); + const [result, search, resetSearch] = useAsyncSearch( + filteredUsers, + getUserIdString, + SEARCH_OPTIONS + ); + const queryHighlighRegex = result?.query ? makeHighlightRegex([result.query]) : undefined; + + const suggestionUsers = result + ? result.items + : filteredUsers.sort((a, b) => (a.toLocaleLowerCase() >= b.toLocaleLowerCase() ? 1 : -1)); + + const handleOpenMenu: MouseEventHandler = (evt) => { + setMenuCords(evt.currentTarget.getBoundingClientRect()); + }; + const handleCloseMenu = () => { + setMenuCords(undefined); + setValidUserId(undefined); + resetSearch(); + }; + + const handleCreatorChange: ChangeEventHandler = (evt) => { + const creatorInput = evt.currentTarget; + const creator = creatorInput.value.trim(); + if (isUserId(creator)) { + setValidUserId(creator); + } else { + setValidUserId(undefined); + const term = + getMxIdLocalPart(creator) ?? (creator.startsWith('@') ? creator.slice(1) : creator); + if (term) { + search(term); + } else { + resetSearch(); + } + } + }; + + const handleSelectUserId = (userId?: string) => { + if (userId && isUserId(userId)) { + onSelect(userId); + handleCloseMenu(); + } + }; + + const handleCreatorKeyDown: KeyboardEventHandler = (evt) => { + if (isKeyHotkey('enter', evt)) { + evt.preventDefault(); + const creator = evt.currentTarget.value.trim(); + handleSelectUserId(isUserId(creator) ? creator : suggestionUsers[0]); + } + }; + + const handleEnterClick = () => { + handleSelectUserId(validUserId); + }; + + return ( + + + + + {mx.getSafeUserId()} + + {additionalCreators.map((creator) => ( + } + onClick={() => onRemove(creator)} + disabled={disabled} + > + {creator} + + ))} + evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + + + + + + + + + + + {!validUserId && suggestionUsers.length > 0 ? ( + + + {suggestionUsers.map((userId) => ( + handleSelectUserId(userId)} + after={ + + {getMxIdServer(userId)} + + } + > + + + + {queryHighlighRegex + ? findAndReplace( + getMxIdLocalPart(userId) ?? userId, + queryHighlighRegex, + (match, pushIndex) => ( + + {match[0]} + + ), + (txt) => txt + ) + : getMxIdLocalPart(userId)} + + + + + ))} + + + ) : ( + + + No Suggestions + + + Please provide the user ID and hit Enter. + + + )} + + + + + } + > + + + + + + + + ); +} diff --git a/src/app/components/create-room/RoomVersionSelector.tsx b/src/app/components/create-room/RoomVersionSelector.tsx index 281f520a..219ded0c 100644 --- a/src/app/components/create-room/RoomVersionSelector.tsx +++ b/src/app/components/create-room/RoomVersionSelector.tsx @@ -47,7 +47,7 @@ export function RoomVersionSelector({ gap="500" > { const content: Record = {}; if (typeof type === 'string') { @@ -23,6 +24,9 @@ export const createRoomCreationContent = ( if (allowFederation === false) { content['m.federate'] = false; } + if (Array.isArray(additionalCreators)) { + content.additional_creators = additionalCreators; + } return content; }; @@ -89,6 +93,7 @@ export type CreateRoomData = { encryption?: boolean; knock: boolean; allowFederation: boolean; + additionalCreators?: string[]; }; export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise => { const initialState: ICreateRoomStateEvent[] = []; @@ -108,7 +113,11 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis name: data.name, topic: data.topic, room_alias_name: data.aliasLocalPart, - creation_content: createRoomCreationContent(data.type, data.allowFederation), + creation_content: createRoomCreationContent( + data.type, + data.allowFederation, + data.additionalCreators + ), initial_state: initialState, }; diff --git a/src/app/components/image-pack-view/RoomImagePack.tsx b/src/app/components/image-pack-view/RoomImagePack.tsx index 9dd45c1f..92b4ff21 100644 --- a/src/app/components/image-pack-view/RoomImagePack.tsx +++ b/src/app/components/image-pack-view/RoomImagePack.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useMemo } from 'react'; import { Room } from 'matrix-js-sdk'; -import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; +import { usePowerLevels } from '../../hooks/usePowerLevels'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { ImagePackContent } from './ImagePackContent'; import { ImagePack, PackContent } from '../../plugins/custom-emoji'; import { StateEvent } from '../../../types/matrix/room'; import { useRoomImagePack } from '../../hooks/useImagePacks'; import { randomStr } from '../../utils/common'; +import { useRoomPermissions } from '../../hooks/useRoomPermissions'; +import { useRoomCreators } from '../../hooks/useRoomCreators'; type RoomImagePackProps = { room: Room; @@ -17,9 +19,10 @@ export function RoomImagePack({ room, stateKey }: RoomImagePackProps) { const mx = useMatrixClient(); const userId = mx.getUserId()!; const powerLevels = usePowerLevels(room); + const creators = useRoomCreators(room); - const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); - const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(userId)); + const permissions = useRoomPermissions(creators, powerLevels); + const canEditImagePack = permissions.stateEvent(StateEvent.PoniesRoomEmotes, userId); const fallbackPack = useMemo(() => { const fakePackId = randomStr(4); diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index dc92bf83..57bf2af9 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -10,8 +10,8 @@ import * as css from './Reply.css'; import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content'; import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser'; import { useRoomEvent } from '../../hooks/useRoomEvent'; -import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags'; import colorMXID from '../../../util/colorMXID'; +import { GetMemberPowerTag } from '../../hooks/useMemberPowerTag'; type ReplyLayoutProps = { userColor?: string; @@ -57,8 +57,7 @@ type ReplyProps = { replyEventId: string; threadRootId?: string | undefined; onClick?: MouseEventHandler | undefined; - getPowerLevel?: (userId: string) => number; - getPowerLevelTag?: GetPowerLevelTag; + getMemberPowerTag?: GetMemberPowerTag; accessibleTagColors?: Map; legacyUsernameColor?: boolean; }; @@ -71,8 +70,7 @@ export const Reply = as<'div', ReplyProps>( replyEventId, threadRootId, onClick, - getPowerLevel, - getPowerLevelTag, + getMemberPowerTag, accessibleTagColors, legacyUsernameColor, ...props @@ -88,8 +86,7 @@ export const Reply = as<'div', ReplyProps>( const { body } = replyEvent?.getContent() ?? {}; const sender = replyEvent?.getSender(); - const senderPL = sender && getPowerLevel?.(sender); - const powerTag = typeof senderPL === 'number' ? getPowerLevelTag?.(senderPL) : undefined; + const powerTag = sender ? getMemberPowerTag?.(sender) : undefined; const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined; const usernameColor = legacyUsernameColor ? colorMXID(sender ?? replyEventId) : tagColor; diff --git a/src/app/components/room-intro/RoomIntro.tsx b/src/app/components/room-intro/RoomIntro.tsx index b02d9f5a..c388efd4 100644 --- a/src/app/components/room-intro/RoomIntro.tsx +++ b/src/app/components/room-intro/RoomIntro.tsx @@ -87,7 +87,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) => {typeof prevRoomId === 'string' && (mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (