From 752a19a4e7f3a25ec5379918a2cfcc75327bde73 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:57:37 +0530 Subject: [PATCH 01/25] Open tombstone space as space (#2428) --- src/app/pages/client/space/Space.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index b657f73e..a335a0ad 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -297,7 +297,7 @@ function SpaceHeader() { type SpaceTombstoneProps = { roomId: string; replacementRoomId: string }; export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProps) { const mx = useMatrixClient(); - const { navigateRoom } = useRoomNavigate(); + const { navigateSpace } = useRoomNavigate(); const [joinState, handleJoin] = useAsyncCallback( useCallback(() => { @@ -311,8 +311,8 @@ export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProp const replacementRoom = mx.getRoom(replacementRoomId); const handleOpen = () => { - if (replacementRoom) navigateRoom(replacementRoom.roomId); - if (joinState.status === AsyncStatus.Success) navigateRoom(joinState.data.roomId); + if (replacementRoom) navigateSpace(replacementRoom.roomId); + if (joinState.status === AsyncStatus.Success) navigateSpace(joinState.data.roomId); }; return ( From 1ad7fe8deb7efb2ee6854f69ee920a1dd910869e Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:00:02 +0530 Subject: [PATCH 02/25] Fix missing creators support using via (#2431) * add additional_creators in IRoomCreateContent type * use creators in getViaServers * consider creators in guessing perfect parent --- src/app/plugins/via-servers.ts | 12 ++++++++-- src/app/utils/room.ts | 44 +++++++++++++++++++++++++++++----- src/types/matrix/room.ts | 1 + 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/app/plugins/via-servers.ts b/src/app/plugins/via-servers.ts index 75470999..d825a1fd 100644 --- a/src/app/plugins/via-servers.ts +++ b/src/app/plugins/via-servers.ts @@ -1,11 +1,19 @@ import { Room } from 'matrix-js-sdk'; import { IPowerLevels } from '../hooks/usePowerLevels'; -import { getMxIdServer } from '../utils/matrix'; -import { StateEvent } from '../../types/matrix/room'; +import { creatorsSupported, getMxIdServer } from '../utils/matrix'; +import { IRoomCreateContent, StateEvent } from '../../types/matrix/room'; import { getStateEvent } from '../utils/room'; export const getViaServers = (room: Room): string[] => { const getHighestPowerUserId = (): string | undefined => { + const creatorEvent = getStateEvent(room, StateEvent.RoomCreate); + if ( + creatorEvent && + creatorsSupported(creatorEvent.getContent().room_version) + ) { + return creatorEvent.getSender(); + } + const powerLevels = getStateEvent(room, StateEvent.RoomPowerLevels)?.getContent(); if (!powerLevels) return undefined; diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index a962c45d..b4bba2ad 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -20,6 +20,7 @@ import { import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; import { AccountDataEvent } from '../../types/matrix/accountData'; import { + IRoomCreateContent, Membership, MessageEvent, NotificationType, @@ -43,7 +44,7 @@ export const getStateEvents = (room: Room, eventType: StateEvent): MatrixEvent[] export const getAccountData = ( mx: MatrixClient, eventType: AccountDataEvent -): MatrixEvent | undefined => mx.getAccountData(eventType); +): MatrixEvent | undefined => mx.getAccountData(eventType as any); export const getMDirects = (mDirectEvent: MatrixEvent): Set => { const roomIds = new Set(); @@ -480,6 +481,23 @@ export const bannedInRooms = (mx: MatrixClient, rooms: string[], otherUserId: st return banned; }); +export const getAllVersionsRoomCreator = (room: Room): Set => { + const creators = new Set(); + + const createEvent = getStateEvent(room, StateEvent.RoomCreate); + const createContent = createEvent?.getContent(); + const creator = createEvent?.getSender(); + if (typeof creator === 'string') creators.add(creator); + + if (createContent && Array.isArray(createContent.additional_creators)) { + createContent.additional_creators.forEach((c) => { + if (typeof c === 'string') creators.add(c); + }); + } + + return creators; +}; + export const guessPerfectParent = ( mx: MatrixClient, roomId: string, @@ -490,15 +508,29 @@ export const guessPerfectParent = ( } const getSpecialUsers = (rId: string): string[] => { + const specialUsers: Set = new Set(); + const r = mx.getRoom(rId); - const powerLevels = - r && getStateEvent(r, StateEvent.RoomPowerLevels)?.getContent(); + if (!r) return []; + + getAllVersionsRoomCreator(r).forEach((c) => specialUsers.add(c)); + + const powerLevels = getStateEvent( + r, + StateEvent.RoomPowerLevels + )?.getContent(); const { users_default: usersDefault, users } = powerLevels ?? {}; - if (typeof users !== 'object') return []; - const defaultPower = typeof usersDefault === 'number' ? usersDefault : 0; - return Object.keys(users).filter((userId) => users[userId] > defaultPower); + + if (typeof users === 'object') + Object.keys(users).forEach((userId) => { + if (users[userId] > defaultPower) { + specialUsers.add(userId); + } + }); + + return Array.from(specialUsers); }; let perfectParent: string | undefined; diff --git a/src/types/matrix/room.ts b/src/types/matrix/room.ts index f0927b3c..b866fd77 100644 --- a/src/types/matrix/room.ts +++ b/src/types/matrix/room.ts @@ -70,6 +70,7 @@ export type IRoomCreateContent = { ['m.federate']?: boolean; room_version: string; type?: string; + additional_creators?: string[]; predecessor?: { event_id?: string; room_id: string; From 50583f94749d479e31d1d2463a335394da47af0c Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:00:52 +0530 Subject: [PATCH 03/25] Fix room v12 mention pills (#2438) --- src/app/plugins/matrix-to.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/plugins/matrix-to.ts b/src/app/plugins/matrix-to.ts index c9df0a87..feeafe05 100644 --- a/src/app/plugins/matrix-to.ts +++ b/src/app/plugins/matrix-to.ts @@ -42,9 +42,9 @@ const MATRIX_TO = /^https?:\/\/matrix\.to\S*$/; export const testMatrixTo = (href: string): boolean => MATRIX_TO.test(href); const MATRIX_TO_USER = /^https?:\/\/matrix\.to\/#\/(@[^:\s]+:[^?/\s]+)\/?$/; -const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/?(\?[\S]*)?$/; +const MATRIX_TO_ROOM = /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/?(\?[\S]*)?$/; const MATRIX_TO_ROOM_EVENT = - /^https?:\/\/matrix\.to\/#\/([#!][^:\s]+:[^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/; + /^https?:\/\/matrix\.to\/#\/([#!][^?/\s]+)\/(\$[^?/\s]+)\/?(\?[\S]*)?$/; export const parseMatrixToUser = (href: string): string | undefined => { const match = href.match(MATRIX_TO_USER); From 544a06964db4cee60154922b41d16e859bc88d6f Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:02:09 +0530 Subject: [PATCH 04/25] Hide block user button for own profile (#2439) --- src/app/components/user-profile/UserRoomProfile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 9f8985af..e92d9098 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -124,8 +124,8 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { {server && } {creator ? : } - - + {userId !== myUserId && } + {userId !== myUserId && } {ignored && } From 63fa60e7f4eae6b6e3537f2c90086e8f47698171 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:04:46 +0530 Subject: [PATCH 05/25] Open user profile at around mouse anchor (#2440) --- src/app/components/event-readers/EventReaders.tsx | 3 ++- src/app/features/common-settings/members/Members.tsx | 3 ++- src/app/features/room/reaction-viewer/ReactionViewer.tsx | 3 ++- src/app/utils/dom.ts | 7 +++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx index 75fdf0da..c7900237 100644 --- a/src/app/components/event-readers/EventReaders.tsx +++ b/src/app/components/event-readers/EventReaders.tsx @@ -23,6 +23,7 @@ import { UserAvatar } from '../user-avatar'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile'; import { useSpaceOptionally } from '../../hooks/useSpace'; +import { getMouseEventCords } from '../../utils/dom'; export type EventReadersProps = { room: Room; @@ -83,7 +84,7 @@ export const EventReaders = as<'div', EventReadersProps>( room.roomId, space?.roomId, readerId, - event.currentTarget.getBoundingClientRect(), + getMouseEventCords(event.nativeEvent), 'Bottom' ); }} diff --git a/src/app/features/common-settings/members/Members.tsx b/src/app/features/common-settings/members/Members.tsx index 156f4f63..9940a751 100644 --- a/src/app/features/common-settings/members/Members.tsx +++ b/src/app/features/common-settings/members/Members.tsx @@ -55,6 +55,7 @@ import { import { useSpaceOptionally } from '../../../hooks/useSpace'; import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../../hooks/useMemberPowerTag'; import { useRoomCreators } from '../../../hooks/useRoomCreators'; +import { getMouseEventCords } from '../../../utils/dom'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, @@ -145,7 +146,7 @@ export function Members({ requestClose }: MembersProps) { const btn = evt.currentTarget as HTMLButtonElement; const userId = btn.getAttribute('data-user-id'); if (userId) { - openProfile(room.roomId, space?.roomId, userId, btn.getBoundingClientRect()); + openProfile(room.roomId, space?.roomId, userId, getMouseEventCords(evt.nativeEvent)); } }; diff --git a/src/app/features/room/reaction-viewer/ReactionViewer.tsx b/src/app/features/room/reaction-viewer/ReactionViewer.tsx index 0e7ca833..6c686bc5 100644 --- a/src/app/features/room/reaction-viewer/ReactionViewer.tsx +++ b/src/app/features/room/reaction-viewer/ReactionViewer.tsx @@ -27,6 +27,7 @@ import { UserAvatar } from '../../../components/user-avatar'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useOpenUserRoomProfile } from '../../../state/hooks/userRoomProfile'; import { useSpaceOptionally } from '../../../hooks/useSpace'; +import { getMouseEventCords } from '../../../utils/dom'; export type ReactionViewerProps = { room: Room; @@ -136,7 +137,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>( room.roomId, space?.roomId, senderId, - event.currentTarget.getBoundingClientRect(), + getMouseEventCords(event.nativeEvent), 'Bottom' ); }} diff --git a/src/app/utils/dom.ts b/src/app/utils/dom.ts index f4c3f719..e467bc58 100644 --- a/src/app/utils/dom.ts +++ b/src/app/utils/dom.ts @@ -224,3 +224,10 @@ export const notificationPermission = (permission: NotificationPermission) => { } return false; }; + +export const getMouseEventCords = (event: MouseEvent) => ({ + x: event.clientX, + y: event.clientY, + width: 0, + height: 0, +}); From 367397fdd4bb1a6eb9bee640cb53a9c3e51fd57e Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:05:34 +0530 Subject: [PATCH 06/25] Fix type error when accessing FileList (#2441) --- src/app/utils/dom.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/utils/dom.ts b/src/app/utils/dom.ts index e467bc58..80db3ae7 100644 --- a/src/app/utils/dom.ts +++ b/src/app/utils/dom.ts @@ -43,6 +43,17 @@ export const canFitInScrollView = ( export type FilesOrFile = T extends true ? File[] : File; +export const getFilesFromFileList = (fileList: FileList): File[] => { + const files: File[] = []; + + for (let i = 0; i < fileList.length; i += 1) { + const file: File | undefined = fileList[i]; + if (file instanceof File) files.push(file); + } + + return files; +}; + export const selectFile = ( accept: string, multiple?: M @@ -58,7 +69,7 @@ export const selectFile = ( if (!fileList) { resolve(undefined); } else { - const files: File[] = [...fileList].filter((file) => file); + const files: File[] = getFilesFromFileList(fileList); resolve((multiple ? files : files[0]) as FilesOrFile); } input.removeEventListener('change', changeHandler); @@ -70,7 +81,7 @@ export const selectFile = ( export const getDataTransferFiles = (dataTransfer: DataTransfer): File[] | undefined => { const fileList = dataTransfer.files; - const files = [...fileList].filter((file) => file); + const files: File[] = getFilesFromFileList(fileList); if (files.length === 0) return undefined; return files; }; From c5d4530947699e8aa932d41259a458f1ee03b6f5 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:10:39 +0530 Subject: [PATCH 07/25] Add new join with address prompt (#2442) --- .../join-address-prompt/JoinAddressPrompt.tsx | 131 ++++++++++++++++++ .../components/join-address-prompt/index.ts | 1 + src/app/pages/client/home/Home.tsx | 75 +++++----- src/app/pages/client/sidebar/CreateTab.tsx | 29 +++- 4 files changed, 201 insertions(+), 35 deletions(-) create mode 100644 src/app/components/join-address-prompt/JoinAddressPrompt.tsx create mode 100644 src/app/components/join-address-prompt/index.ts diff --git a/src/app/components/join-address-prompt/JoinAddressPrompt.tsx b/src/app/components/join-address-prompt/JoinAddressPrompt.tsx new file mode 100644 index 00000000..50a89418 --- /dev/null +++ b/src/app/components/join-address-prompt/JoinAddressPrompt.tsx @@ -0,0 +1,131 @@ +import React, { FormEventHandler, useState } from 'react'; +import FocusTrap from 'focus-trap-react'; +import { + Dialog, + Overlay, + OverlayCenter, + OverlayBackdrop, + Header, + config, + Box, + Text, + IconButton, + Icon, + Icons, + Button, + Input, + color, +} from 'folds'; +import { stopPropagation } from '../../utils/keyboard'; +import { isRoomAlias, isRoomId } from '../../utils/matrix'; +import { parseMatrixToRoom, parseMatrixToRoomEvent, testMatrixTo } from '../../plugins/matrix-to'; +import { tryDecodeURIComponent } from '../../utils/dom'; + +type JoinAddressProps = { + onOpen: (roomIdOrAlias: string, via?: string[], eventId?: string) => void; + onCancel: () => void; +}; +export function JoinAddressPrompt({ onOpen, onCancel }: JoinAddressProps) { + const [invalid, setInvalid] = useState(false); + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + setInvalid(false); + + const target = evt.target as HTMLFormElement | undefined; + const addressInput = target?.addressInput as HTMLInputElement | undefined; + const address = addressInput?.value.trim(); + if (!address) return; + + if (isRoomId(address) || isRoomAlias(address)) { + onOpen(address); + return; + } + + if (testMatrixTo(address)) { + const decodedAddress = tryDecodeURIComponent(address); + const toRoom = parseMatrixToRoom(decodedAddress); + if (toRoom) { + onOpen(toRoom.roomIdOrAlias, toRoom.viaServers); + return; + } + + const toEvent = parseMatrixToRoomEvent(decodedAddress); + if (toEvent) { + onOpen(toEvent.roomIdOrAlias, toEvent.viaServers, toEvent.eventId); + return; + } + } + + setInvalid(true); + }; + + return ( + }> + + + +
+ + Join with Address + + + + +
+ + + + Enter public address to join the community. Addresses looks like: + + +
  • #community:server
  • +
  • https://matrix.to/#/#community:server
  • +
  • https://matrix.to/#/!xYzAj?via=server
  • +
    +
    + + Address + + {invalid && ( + + Invalid Address + + )} + + +
    +
    +
    +
    +
    + ); +} diff --git a/src/app/components/join-address-prompt/index.ts b/src/app/components/join-address-prompt/index.ts new file mode 100644 index 00000000..b14b8a61 --- /dev/null +++ b/src/app/components/join-address-prompt/index.ts @@ -0,0 +1 @@ +export * from './JoinAddressPrompt'; diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx index d2333919..2597bb73 100644 --- a/src/app/pages/client/home/Home.tsx +++ b/src/app/pages/client/home/Home.tsx @@ -30,10 +30,12 @@ import { NavLink, } from '../../../components/nav'; import { + encodeSearchParamValueArray, getExplorePath, getHomeCreatePath, getHomeRoomPath, getHomeSearchPath, + withSearchParam, } from '../../pathUtils'; import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; @@ -49,7 +51,6 @@ import { makeNavCategoryId } from '../../../state/closedNavCategories'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { useCategoryHandler } from '../../../hooks/useCategoryHandler'; import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; -import { openJoinAlias } from '../../../../client/action/navigation'; import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page'; import { useRoomsUnread } from '../../../state/hooks/unread'; import { markAsRead } from '../../../../client/action/notifications'; @@ -61,6 +62,9 @@ import { getRoomNotificationMode, useRoomsNotificationPreferencesContext, } from '../../../hooks/useRoomsNotificationPreferences'; +import { UseStateProvider } from '../../../components/UseStateProvider'; +import { JoinAddressPrompt } from '../../../components/join-address-prompt'; +import { _RoomSearchParams } from '../../paths'; type HomeMenuProps = { requestClose: () => void; @@ -77,11 +81,6 @@ const HomeMenu = forwardRef(({ requestClose }, re requestClose(); }; - const handleJoinAddress = () => { - openJoinAlias(); - requestClose(); - }; - return ( @@ -96,16 +95,6 @@ const HomeMenu = forwardRef(({ requestClose }, re Mark as Read - } - > - - Join with Address - - ); @@ -268,22 +257,44 @@ export function Home() { - - openJoinAlias()}> - - - - - - - - Join with Address - - - - - - + + {(open, setOpen) => ( + <> + + setOpen(true)}> + + + + + + + + Join with Address + + + + + + + {open && ( + setOpen(false)} + onOpen={(roomIdOrAlias, viaServers, eventId) => { + setOpen(false); + const path = getHomeRoomPath(roomIdOrAlias, eventId); + navigate( + viaServers + ? withSearchParam<_RoomSearchParams>(path, { + viaServers: encodeSearchParamValueArray(viaServers), + }) + : path + ); + }} + /> + )} + + )} + diff --git a/src/app/pages/client/sidebar/CreateTab.tsx b/src/app/pages/client/sidebar/CreateTab.tsx index a7f9350c..e6575cb4 100644 --- a/src/app/pages/client/sidebar/CreateTab.tsx +++ b/src/app/pages/client/sidebar/CreateTab.tsx @@ -7,15 +7,22 @@ import { stopPropagation } from '../../../utils/keyboard'; import { SequenceCard } from '../../../components/sequence-card'; import { SettingTile } from '../../../components/setting-tile'; import { ContainerColor } from '../../../styles/ContainerColor.css'; -import { openJoinAlias } from '../../../../client/action/navigation'; -import { getCreatePath } from '../../pathUtils'; +import { + encodeSearchParamValueArray, + getCreatePath, + getSpacePath, + withSearchParam, +} from '../../pathUtils'; import { useCreateSelected } from '../../../hooks/router/useCreateSelected'; +import { JoinAddressPrompt } from '../../../components/join-address-prompt'; +import { _RoomSearchParams } from '../../paths'; export function CreateTab() { const createSelected = useCreateSelected(); const navigate = useNavigate(); const [menuCords, setMenuCords] = useState(); + const [joinAddress, setJoinAddress] = useState(false); const handleMenu: MouseEventHandler = (evt) => { setMenuCords(menuCords ? undefined : evt.currentTarget.getBoundingClientRect()); @@ -27,7 +34,7 @@ export function CreateTab() { }; const handleJoinWithAddress = () => { - openJoinAlias(); + setJoinAddress(true); setMenuCords(undefined); }; @@ -103,6 +110,22 @@ export function CreateTab() { > + {joinAddress && ( + setJoinAddress(false)} + onOpen={(roomIdOrAlias, viaServers) => { + setJoinAddress(false); + const path = getSpacePath(roomIdOrAlias); + navigate( + viaServers + ? withSearchParam<_RoomSearchParams>(path, { + viaServers: encodeSearchParamValueArray(viaServers), + }) + : path + ); + }} + /> + )} )} From 802357b7a07bab19a92f4328d492b275a5ecd278 Mon Sep 17 00:00:00 2001 From: Krishan <33421343+kfiven@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:50:17 +1000 Subject: [PATCH 08/25] Rename the PL 150 to Manager (#2443) Manager seem more appropriate than Co-Founder. As Co-founder essentially have same power to Founder. --- src/app/hooks/usePowerLevelTags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hooks/usePowerLevelTags.ts b/src/app/hooks/usePowerLevelTags.ts index 10235b52..0a6cca50 100644 --- a/src/app/hooks/usePowerLevelTags.ts +++ b/src/app/hooks/usePowerLevelTags.ts @@ -51,7 +51,7 @@ const DEFAULT_TAGS: PowerLevelTags = { color: '#ff6a00', }, 150: { - name: 'Co-Founder', + name: 'Manager', color: '#ff6a7f', }, 101: { From abd713d693f538c9d3092638477645dae7caf61b Mon Sep 17 00:00:00 2001 From: Krishan <33421343+kfiven@users.noreply.github.com> Date: Sun, 17 Aug 2025 21:08:35 +1000 Subject: [PATCH 09/25] Release v4.9.1 (#2446) --- package-lock.json | 4 ++-- package.json | 2 +- src/app/pages/auth/AuthFooter.tsx | 2 +- src/app/pages/client/WelcomePage.tsx | 2 +- src/client/state/cons.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f3daa6c..70826ae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cinny", - "version": "4.9.0", + "version": "4.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cinny", - "version": "4.9.0", + "version": "4.9.1", "license": "AGPL-3.0-only", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "1.1.6", diff --git a/package.json b/package.json index 49d334bf..f1816cdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cinny", - "version": "4.9.0", + "version": "4.9.1", "description": "Yet another matrix client", "main": "index.js", "type": "module", diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx index 18edda39..53191f33 100644 --- a/src/app/pages/auth/AuthFooter.tsx +++ b/src/app/pages/auth/AuthFooter.tsx @@ -15,7 +15,7 @@ export function AuthFooter() { target="_blank" rel="noreferrer" > - v4.9.0 + v4.9.1 Twitter diff --git a/src/app/pages/client/WelcomePage.tsx b/src/app/pages/client/WelcomePage.tsx index c93a4de2..55456855 100644 --- a/src/app/pages/client/WelcomePage.tsx +++ b/src/app/pages/client/WelcomePage.tsx @@ -24,7 +24,7 @@ export function WelcomePage() { target="_blank" rel="noreferrer noopener" > - v4.9.0 + v4.9.1 } diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 9edb67f7..6f4fabe4 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -1,5 +1,5 @@ const cons = { - version: '4.9.0', + version: '4.9.1', secretKey: { ACCESS_TOKEN: 'cinny_access_token', DEVICE_ID: 'cinny_device_id', From 789da641c1209c866388b8e06c012daad72106b6 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:06:57 +0530 Subject: [PATCH 10/25] Fix incorrectly parsed mxid (#2452) --- src/app/utils/matrix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index c8b104d9..b4e2e6b8 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -23,7 +23,7 @@ const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/; export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName); -const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@$+#])(.+):(\S+)$/); +const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@$+#])([^\s:]+):(\S+)$/); const validMxId = (id: string): boolean => !!matchMxId(id); From 09b88d164fabe13440a74dfb3137ba0e57e4e7f1 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:08:46 +0530 Subject: [PATCH 11/25] Fix message button opens left dm room (#2453) --- src/app/utils/matrix.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index b4e2e6b8..a8031202 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -17,7 +17,7 @@ import to from 'await-to-js'; import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common'; import { AccountDataEvent } from '../../types/matrix/accountData'; import { getStateEvent } from './room'; -import { StateEvent } from '../../types/matrix/room'; +import { Membership, StateEvent } from '../../types/matrix/room'; const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/; @@ -182,7 +182,12 @@ export const eventWithShortcode = (ev: MatrixEvent) => export const getDMRoomFor = (mx: MatrixClient, userId: string): Room | undefined => { const dmLikeRooms = mx .getRooms() - .filter((room) => room.hasEncryptionStateEvent() && room.getMembers().length <= 2); + .filter( + (room) => + room.getMyMembership() === Membership.Join && + room.hasEncryptionStateEvent() && + room.getMembers().length <= 2 + ); return dmLikeRooms.find((room) => room.getMember(userId)); }; From 78a0d11f24cc1b7e37930fd3dc53ea54ac1fee53 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Tue, 19 Aug 2025 18:09:31 +0530 Subject: [PATCH 12/25] New add existing room/space modal (#2451) --- .../create-room/AdditionalCreatorInput.tsx | 18 +- src/app/features/add-existing/AddExisting.tsx | 375 ++++++++++++++++++ src/app/features/add-existing/index.ts | 1 + .../features/create-room/CreateRoomModal.tsx | 1 - src/app/features/lobby/SpaceItem.tsx | 14 +- 5 files changed, 390 insertions(+), 19 deletions(-) create mode 100644 src/app/features/add-existing/AddExisting.tsx create mode 100644 src/app/features/add-existing/index.ts diff --git a/src/app/components/create-room/AdditionalCreatorInput.tsx b/src/app/components/create-room/AdditionalCreatorInput.tsx index 51334b49..936b9b94 100644 --- a/src/app/components/create-room/AdditionalCreatorInput.tsx +++ b/src/app/components/create-room/AdditionalCreatorInput.tsx @@ -30,9 +30,7 @@ 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'; +import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; export const useAdditionalCreators = (defaultCreators?: string[]) => { const mx = useMatrixClient(); @@ -245,19 +243,9 @@ export function AdditionalCreatorInput({ {queryHighlighRegex - ? findAndReplace( + ? highlightText(queryHighlighRegex, [ getMxIdLocalPart(userId) ?? userId, - queryHighlighRegex, - (match, pushIndex) => ( - - {match[0]} - - ), - (txt) => txt - ) + ]) : getMxIdLocalPart(userId)} diff --git a/src/app/features/add-existing/AddExisting.tsx b/src/app/features/add-existing/AddExisting.tsx new file mode 100644 index 00000000..cbae018f --- /dev/null +++ b/src/app/features/add-existing/AddExisting.tsx @@ -0,0 +1,375 @@ +import FocusTrap from 'focus-trap-react'; +import { + Avatar, + Box, + Button, + config, + Header, + Icon, + IconButton, + Icons, + Input, + Menu, + MenuItem, + Modal, + Overlay, + OverlayBackdrop, + OverlayCenter, + Scroll, + Spinner, + Text, +} from 'folds'; +import React, { + ChangeEventHandler, + MouseEventHandler, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; +import { useAtomValue } from 'jotai'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { Room } from 'matrix-js-sdk'; +import { stopPropagation } from '../../utils/keyboard'; +import { useDirects, useRooms, useSpaces } from '../../state/hooks/roomList'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { allRoomsAtom } from '../../state/room-list/roomList'; +import { mDirectAtom } from '../../state/mDirectList'; +import { roomToParentsAtom } from '../../state/room/roomToParents'; +import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom'; +import { VirtualTile } from '../../components/virtualizer'; +import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room'; +import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; +import { nameInitials } from '../../utils/common'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { factoryRoomIdByAtoZ } from '../../utils/sort'; +import { + SearchItemStrGetter, + useAsyncSearch, + UseAsyncSearchOptions, +} from '../../hooks/useAsyncSearch'; +import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { StateEvent } from '../../../types/matrix/room'; +import { getViaServers } from '../../plugins/via-servers'; +import { rateLimitedActions } from '../../utils/matrix'; +import { useAlive } from '../../hooks/useAlive'; + +const SEARCH_OPTS: UseAsyncSearchOptions = { + limit: 500, + matchOptions: { + contain: true, + }, + normalizeOptions: { + ignoreWhitespace: false, + }, +}; + +type AddExistingModalProps = { + parentId: string; + space?: boolean; + requestClose: () => void; +}; +export function AddExistingModal({ parentId, space, requestClose }: AddExistingModalProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const alive = useAlive(); + + const mDirects = useAtomValue(mDirectAtom); + const spaces = useSpaces(mx, allRoomsAtom); + const rooms = useRooms(mx, allRoomsAtom, mDirects); + const directs = useDirects(mx, allRoomsAtom, mDirects); + const roomIdToParents = useAtomValue(roomToParentsAtom); + const scrollRef = useRef(null); + + const [selected, setSelected] = useState([]); + + const allRoomsSet = useAllJoinedRoomsSet(); + const getRoom = useGetRoom(allRoomsSet); + + const allItems: string[] = useMemo(() => { + const rIds = space ? [...spaces] : [...rooms, ...directs]; + + return rIds + .filter((rId) => rId !== parentId && !roomIdToParents.get(rId)?.has(parentId)) + .sort(factoryRoomIdByAtoZ(mx)); + }, [spaces, rooms, directs, space, parentId, roomIdToParents, mx]); + + const getRoomNameStr: SearchItemStrGetter = useCallback( + (rId) => getRoom(rId)?.name ?? rId, + [getRoom] + ); + + const [searchResult, searchRoom, resetSearch] = useAsyncSearch( + allItems, + getRoomNameStr, + SEARCH_OPTS + ); + const queryHighlighRegex = searchResult?.query + ? makeHighlightRegex(searchResult.query.split(' ')) + : undefined; + + const items = searchResult ? searchResult.items : allItems; + + const virtualizer = useVirtualizer({ + count: items.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 32, + overscan: 5, + }); + const vItems = virtualizer.getVirtualItems(); + + const handleSearchChange: ChangeEventHandler = (evt) => { + const value = evt.currentTarget.value.trim(); + if (!value) { + resetSearch(); + return; + } + searchRoom(value); + }; + + const [applyState, applyChanges] = useAsyncCallback( + useCallback( + async (selectedRooms) => { + await rateLimitedActions(selectedRooms, async (room) => { + const via = getViaServers(room); + + await mx.sendStateEvent( + parentId, + StateEvent.SpaceChild as any, + { + auto_join: false, + suggested: false, + via, + }, + room.roomId + ); + }); + }, + [mx, parentId] + ) + ); + const applyingChanges = applyState.status === AsyncStatus.Loading; + + const handleRoomClick: MouseEventHandler = (evt) => { + const roomId = evt.currentTarget.getAttribute('data-room-id'); + if (!roomId) return; + if (selected?.includes(roomId)) { + setSelected(selected?.filter((rId) => rId !== roomId)); + return; + } + const addedRooms = [...(selected ?? [])]; + addedRooms.push(roomId); + setSelected(addedRooms); + }; + + const handleApplyChanges = () => { + const selectedRooms = selected.map((rId) => getRoom(rId)).filter((room) => room !== undefined); + applyChanges(selectedRooms).then(() => { + if (alive()) { + setSelected([]); + requestClose(); + } + }); + }; + + const resetChanges = () => { + setSelected([]); + }; + + return ( + }> + + + + +
    + + Add Existing + + + + + + +
    + + + + + } + placeholder="Search" + size="400" + variant="Background" + outlined + /> + + {vItems.length === 0 && ( + + + {searchResult ? 'No Match Found' : `No ${space ? 'Spaces' : 'Rooms'}`} + + + {searchResult + ? `No match found for "${searchResult.query}".` + : `You do not have any ${space ? 'Spaces' : 'Rooms'} to display yet.`} + + + )} + + {vItems.map((vItem) => { + const roomId = items[vItem.index]; + const room = getRoom(roomId); + if (!room) return null; + const selectedItem = selected?.includes(roomId); + const dm = mDirects.has(room.roomId); + + return ( + + + {dm || room.isSpaceRoom() ? ( + ( + + {nameInitials(room.name)} + + )} + /> + ) : ( + + )} + + } + after={selectedItem && } + > + + + {queryHighlighRegex + ? highlightText(queryHighlighRegex, [room.name]) + : room.name} + + + + + ); + })} + + {selected.length > 0 && ( + + + + {applyState.status === AsyncStatus.Error ? ( + + Failed to apply changes! Please try again. + + ) : ( + + Apply when ready. ({selected.length} Selected) + + )} + + + + + + + + )} + + + +
    +
    +
    +
    +
    + ); +} diff --git a/src/app/features/add-existing/index.ts b/src/app/features/add-existing/index.ts new file mode 100644 index 00000000..3607c062 --- /dev/null +++ b/src/app/features/add-existing/index.ts @@ -0,0 +1 @@ +export * from './AddExisting'; diff --git a/src/app/features/create-room/CreateRoomModal.tsx b/src/app/features/create-room/CreateRoomModal.tsx index c1c9ba3e..c9919ba9 100644 --- a/src/app/features/create-room/CreateRoomModal.tsx +++ b/src/app/features/create-room/CreateRoomModal.tsx @@ -54,7 +54,6 @@ function CreateRoomModal({ state }: CreateRoomModalProps) { style={{ padding: config.space.S200, paddingLeft: config.space.S400, - borderBottomWidth: config.borderWidth.B300, }} > diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx index e881a971..64a97900 100644 --- a/src/app/features/lobby/SpaceItem.tsx +++ b/src/app/features/lobby/SpaceItem.tsx @@ -30,12 +30,12 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import * as css from './SpaceItem.css'; import * as styleCss from './style.css'; import { useDraggableItem } from './DnD'; -import { openSpaceAddExisting } from '../../../client/action/navigation'; import { stopPropagation } from '../../utils/keyboard'; import { mxcUrlToHttp } from '../../utils/matrix'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal'; import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal'; +import { AddExistingModal } from '../add-existing'; function SpaceProfileLoading() { return ( @@ -243,6 +243,7 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP function AddRoomButton({ item }: { item: HierarchyItem }) { const [cords, setCords] = useState(); const openCreateRoomModal = useOpenCreateRoomModal(); + const [addExisting, setAddExisting] = useState(false); const handleAddRoom: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); @@ -254,7 +255,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { }; const handleAddExisting = () => { - openSpaceAddExisting(item.roomId); + setAddExisting(true); setCords(undefined); }; @@ -300,6 +301,9 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { > Add Room + {addExisting && ( + setAddExisting(false)} /> + )} ); } @@ -307,6 +311,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) { function AddSpaceButton({ item }: { item: HierarchyItem }) { const [cords, setCords] = useState(); const openCreateSpaceModal = useOpenCreateSpaceModal(); + const [addExisting, setAddExisting] = useState(false); const handleAddSpace: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); @@ -318,7 +323,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) { }; const handleAddExisting = () => { - openSpaceAddExisting(item.roomId, true); + setAddExisting(true); setCords(undefined); }; return ( @@ -363,6 +368,9 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) { > Add Space + {addExisting && ( + setAddExisting(false)} /> + )} ); } From c881b5995725246b4bba2b522b21c3b3abba8b7d Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 24 Aug 2025 18:03:20 +0530 Subject: [PATCH 13/25] =?UTF-8?q?Fix=20image=20overlap=20with=20=E2=80=9CM?= =?UTF-8?q?ark=20as=20read=E2=80=9D=20and=20typing=20indicator=20(#2457)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/message/content/style.css.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/message/content/style.css.ts b/src/app/components/message/content/style.css.ts index 93f3649c..bb5d8484 100644 --- a/src/app/components/message/content/style.css.ts +++ b/src/app/components/message/content/style.css.ts @@ -16,7 +16,6 @@ export const AbsoluteContainer = style([ position: 'absolute', top: 0, left: 0, - zIndex: 1, width: '100%', height: '100%', }, @@ -26,6 +25,7 @@ export const AbsoluteFooter = style([ DefaultReset, { position: 'absolute', + pointerEvents: 'none', bottom: config.space.S100, left: config.space.S100, right: config.space.S100, From 13cdcbcdb167cdf4f8bb124f922a078f36ebdad1 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sun, 24 Aug 2025 18:04:21 +0530 Subject: [PATCH 14/25] New invite user to room dialog (#2460) * fix 0 displayed in invite with no timestamp * support displaying invite reason for receiver * show invite reason as compact message * remove unused import * revert: show invite reason as compact message * remove unused import * add new invite prompt --- .../invite-user-prompt/InviteUserPrompt.tsx | 291 ++++++++++++++++++ .../components/invite-user-prompt/index.ts | 1 + src/app/components/room-intro/RoomIntro.tsx | 16 +- src/app/features/lobby/HierarchyItemMenu.tsx | 45 ++- src/app/features/lobby/LobbyHeader.tsx | 17 +- src/app/features/room-nav/RoomNavItem.tsx | 17 +- src/app/features/room/RoomViewHeader.tsx | 17 +- src/app/pages/client/inbox/Invites.tsx | 52 ++-- src/app/pages/client/sidebar/SpaceTabs.tsx | 17 +- src/app/pages/client/space/Space.tsx | 17 +- 10 files changed, 434 insertions(+), 56 deletions(-) create mode 100644 src/app/components/invite-user-prompt/InviteUserPrompt.tsx create mode 100644 src/app/components/invite-user-prompt/index.ts diff --git a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx new file mode 100644 index 00000000..82313c3e --- /dev/null +++ b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx @@ -0,0 +1,291 @@ +import React, { + ChangeEventHandler, + FormEventHandler, + KeyboardEventHandler, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; +import { + Overlay, + OverlayBackdrop, + OverlayCenter, + Box, + Header, + config, + Text, + IconButton, + Icon, + Icons, + Input, + Button, + Spinner, + color, + TextArea, + Dialog, + Menu, + toRem, + Scroll, + MenuItem, +} from 'folds'; +import { Room } from 'matrix-js-sdk'; +import { isKeyHotkey } from 'is-hotkey'; +import FocusTrap from 'focus-trap-react'; +import { stopPropagation } from '../../utils/keyboard'; +import { useDirectUsers } from '../../hooks/useDirectUsers'; +import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; +import { Membership } from '../../../types/matrix/room'; +import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; +import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { BreakWord } from '../../styles/Text.css'; +import { useAlive } from '../../hooks/useAlive'; + +const SEARCH_OPTIONS: UseAsyncSearchOptions = { + limit: 1000, + matchOptions: { + contain: true, + }, +}; +const getUserIdString = (userId: string) => getMxIdLocalPart(userId) ?? userId; + +type InviteUserProps = { + room: Room; + requestClose: () => void; +}; +export function InviteUserPrompt({ room, requestClose }: InviteUserProps) { + const mx = useMatrixClient(); + const alive = useAlive(); + + const inputRef = useRef(null); + const directUsers = useDirectUsers(); + const [validUserId, setValidUserId] = useState(); + + const filteredUsers = useMemo( + () => + directUsers.filter((userId) => { + const membership = room.getMember(userId)?.membership; + return membership !== Membership.Join; + }), + [directUsers, room] + ); + const [result, search, resetSearch] = useAsyncSearch( + filteredUsers, + getUserIdString, + SEARCH_OPTIONS + ); + const queryHighlighRegex = result?.query + ? makeHighlightRegex(result.query.split(' ')) + : undefined; + + const [inviteState, invite] = useAsyncCallback( + useCallback( + async (userId, reason) => { + await mx.invite(room.roomId, userId, reason); + }, + [mx, room] + ) + ); + + const inviting = inviteState.status === AsyncStatus.Loading; + + const handleReset = () => { + if (inputRef.current) inputRef.current.value = ''; + setValidUserId(undefined); + resetSearch(); + }; + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + const target = evt.target as HTMLFormElement | undefined; + + if (inviting || !validUserId) return; + + const reasonInput = target?.reasonInput as HTMLTextAreaElement | undefined; + const reason = reasonInput?.value.trim(); + + invite(validUserId, reason || undefined).then(() => { + if (alive()) { + handleReset(); + if (reasonInput) reasonInput.value = ''; + } + }); + }; + + const handleSearchChange: ChangeEventHandler = (evt) => { + const value = evt.currentTarget.value.trim(); + if (isUserId(value)) { + setValidUserId(value); + } else { + setValidUserId(undefined); + const term = getMxIdLocalPart(value) ?? (value.startsWith('@') ? value.slice(1) : value); + if (term) { + search(term); + } else { + resetSearch(); + } + } + }; + + const handleUserId = (userId: string) => { + if (inputRef.current) { + inputRef.current.value = userId; + setValidUserId(userId); + resetSearch(); + inputRef.current.focus(); + } + }; + + const handleKeyDown: KeyboardEventHandler = (evt) => { + if (isKeyHotkey('escape', evt)) { + resetSearch(); + return; + } + if (isKeyHotkey('tab', evt) && result && result.items.length > 0) { + evt.preventDefault(); + const userId = result.items[0]; + handleUserId(userId); + } + }; + + return ( + }> + + inputRef.current, + clickOutsideDeactivates: true, + onDeactivate: requestClose, + escapeDeactivates: stopPropagation, + }} + > + + +
    + + + Invite + + + + + + + +
    + + + User ID +
    + + {result && result.items.length > 0 && ( + isKeyHotkey('arrowdown', evt), + isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), + escapeDeactivates: stopPropagation, + }} + > + + + +
    + {result.items.map((userId) => { + const username = `${getMxIdLocalPart(userId)}`; + const userServer = getMxIdServer(userId); + + return ( + handleUserId(userId)} + after={ + + {userServer} + + } + disabled={inviting} + > + + + + {queryHighlighRegex + ? highlightText(queryHighlighRegex, [ + username ?? userId, + ]) + : username} + + + + + ); + })} +
    +
    +
    +
    +
    + )} +
    +
    + + Reason (Optional) +