diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index b8b34d78..78d201ec 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -1,7 +1,8 @@ -import { Box, Button, color, config, Icon, Icons, Spinner, Text } from 'folds'; -import React, { useCallback } from 'react'; +import { Box, Button, config, Icon, Icons, Text } from 'folds'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; import { UserHero, UserHeroName } from './UserHero'; -import { getDMRoomFor, getMxIdServer, mxcUrlToHttp } from '../../utils/matrix'; +import { getMxIdServer, mxcUrlToHttp } from '../../utils/matrix'; import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; @@ -9,11 +10,6 @@ import { usePowerLevels } from '../../hooks/usePowerLevels'; import { useRoom } from '../../hooks/useRoom'; import { useUserPresence } from '../../hooks/useUserPresence'; import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; -import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; -import { createDM } from '../../../client/action/room'; -import { hasDevices } from '../../../util/matrixUtil'; -import { useRoomNavigate } from '../../hooks/useRoomNavigate'; -import { useAlive } from '../../hooks/useAlive'; import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile'; import { PowerChip } from './PowerChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; @@ -24,6 +20,8 @@ import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare'; import { CreatorChip } from './CreatorChip'; +import { getDirectCreatePath, withSearchParam } from '../../pages/pathUtils'; +import { DirectCreateSearchParams } from '../../pages/paths'; type UserRoomProfileProps = { userId: string; @@ -31,8 +29,7 @@ type UserRoomProfileProps = { export function UserRoomProfile({ userId }: UserRoomProfileProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); - const { navigateRoom } = useRoomNavigate(); - const alive = useAlive(); + const navigate = useNavigate(); const closeUserRoomProfile = useCloseUserRoomProfile(); const ignoredUsers = useIgnoredUsers(); const ignored = ignoredUsers.includes(userId); @@ -62,26 +59,12 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { const presence = useUserPresence(userId); - const [directMessageState, directMessage] = useAsyncCallback( - useCallback(async () => { - const result = await createDM(mx, userId, await hasDevices(mx, userId)); - return result.room_id as string; - }, [userId, mx]) - ); - const handleMessage = () => { - const dmRoomId = getDMRoomFor(mx, userId)?.roomId; - if (dmRoomId) { - navigateRoom(dmRoomId); - closeUserRoomProfile(); - return; - } - directMessage().then((rId) => { - if (alive()) { - navigateRoom(rId); - closeUserRoomProfile(); - } - }); + closeUserRoomProfile(); + const directSearchParam: DirectCreateSearchParams = { + userId, + }; + navigate(withSearchParam(getDirectCreatePath(), directSearchParam)); }; return ( @@ -102,14 +85,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { variant="Primary" fill="Solid" radii="300" - disabled={directMessageState.status === AsyncStatus.Loading} - before={ - directMessageState.status === AsyncStatus.Loading ? ( - - ) : ( - - ) - } + before={} onClick={handleMessage} > Message @@ -117,11 +93,6 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { )} - {directMessageState.status === AsyncStatus.Error && ( - - {directMessageState.error.message} - - )} {server && } diff --git a/src/app/features/create-chat/CreateChat.tsx b/src/app/features/create-chat/CreateChat.tsx new file mode 100644 index 00000000..eccd4e66 --- /dev/null +++ b/src/app/features/create-chat/CreateChat.tsx @@ -0,0 +1,150 @@ +import { Box, Button, color, config, Icon, Icons, Input, Spinner, Switch, Text } from 'folds'; +import React, { FormEventHandler, useCallback, useState } from 'react'; +import { ICreateRoomStateEvent, MatrixError, Preset, Visibility } from 'matrix-js-sdk'; +import { useNavigate } from 'react-router-dom'; +import { SettingTile } from '../../components/setting-tile'; +import { SequenceCard } from '../../components/sequence-card'; +import { addRoomIdToMDirect, isUserId } from '../../utils/matrix'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { ErrorCode } from '../../cs-errorcode'; +import { millisecondsToMinutes } from '../../utils/common'; +import { createRoomEncryptionState } from '../../components/create-room'; +import { useAlive } from '../../hooks/useAlive'; +import { getDirectRoomPath } from '../../pages/pathUtils'; + +type CreateChatProps = { + defaultUserId?: string; +}; +export function CreateChat({ defaultUserId }: CreateChatProps) { + const mx = useMatrixClient(); + const alive = useAlive(); + const navigate = useNavigate(); + + const [encryption, setEncryption] = useState(true); + const [invalidUserId, setInvalidUserId] = useState(false); + + const [createState, create] = useAsyncCallback( + useCallback( + async (userId, encrypted) => { + const initialState: ICreateRoomStateEvent[] = []; + + if (encrypted) initialState.push(createRoomEncryptionState()); + + const result = await mx.createRoom({ + is_direct: true, + invite: [userId], + visibility: Visibility.Private, + preset: Preset.TrustedPrivateChat, + initial_state: initialState, + }); + + addRoomIdToMDirect(mx, result.room_id, userId); + + return result.room_id; + }, + [mx] + ) + ); + const loading = createState.status === AsyncStatus.Loading; + const error = createState.status === AsyncStatus.Error ? createState.error : undefined; + const disabled = createState.status === AsyncStatus.Loading; + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + setInvalidUserId(false); + + const target = evt.target as HTMLFormElement | undefined; + const userIdInput = target?.userIdInput as HTMLInputElement | undefined; + const userId = userIdInput?.value.trim(); + + if (!userIdInput || !userId) return; + if (!isUserId(userId)) { + setInvalidUserId(true); + return; + } + + create(userId, encryption).then((roomId) => { + if (alive()) { + userIdInput.value = ''; + navigate(getDirectRoomPath(roomId)); + } + }); + }; + + return ( + + + User ID + + {invalidUserId && ( + + + + Please enter a valid User ID. + + + )} + + + Options + + + } + /> + + + {error && ( + + + + + {error instanceof MatrixError && error.name === ErrorCode.M_LIMIT_EXCEEDED + ? `Server rate-limited your request for ${millisecondsToMinutes( + (error.data.retry_after_ms as number | undefined) ?? 0 + )} minutes!` + : error.message} + + + + )} + + + + + ); +} diff --git a/src/app/features/create-chat/index.ts b/src/app/features/create-chat/index.ts new file mode 100644 index 00000000..ed988dfb --- /dev/null +++ b/src/app/features/create-chat/index.ts @@ -0,0 +1 @@ +export * from './CreateChat'; diff --git a/src/app/hooks/useCommands.ts b/src/app/hooks/useCommands.ts index c95142e8..4d70465d 100644 --- a/src/app/hooks/useCommands.ts +++ b/src/app/hooks/useCommands.ts @@ -2,12 +2,15 @@ import { Direction, IContextResponse, MatrixClient, Method, Room, RoomMember } f import { RoomServerAclEventContent } from 'matrix-js-sdk/lib/types'; import { useMemo } from 'react'; import { + addRoomIdToMDirect, getDMRoomFor, + guessDmRoomUserId, isRoomAlias, isRoomId, isServerName, isUserId, rateLimitedActions, + removeRoomIdFromMDirect, } from '../utils/matrix'; import { hasDevices } from '../../util/matrixUtil'; import * as roomActions from '../../client/action/room'; @@ -348,14 +351,15 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => { name: Command.ConvertToDm, description: 'Convert room to direct message', exe: async () => { - roomActions.convertToDm(mx, room.roomId); + const dmUserId = guessDmRoomUserId(room, mx.getSafeUserId()); + await addRoomIdToMDirect(mx, room.roomId, dmUserId); }, }, [Command.ConvertToRoom]: { name: Command.ConvertToRoom, description: 'Convert direct message to room', exe: async () => { - roomActions.convertToRoom(mx, room.roomId); + await removeRoomIdFromMDirect(mx, room.roomId); }, }, [Command.Delete]: { diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx index b6a8de1a..51de3946 100644 --- a/src/app/pages/client/direct/Direct.tsx +++ b/src/app/pages/client/direct/Direct.tsx @@ -17,6 +17,7 @@ import { } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import FocusTrap from 'focus-trap-react'; +import { useNavigate } from 'react-router-dom'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { factoryRoomIdByActivity } from '../../../utils/sort'; import { @@ -28,7 +29,7 @@ import { NavItem, NavItemContent, } from '../../../components/nav'; -import { getDirectRoomPath } from '../../pathUtils'; +import { getDirectCreatePath, getDirectRoomPath } from '../../pathUtils'; import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { VirtualTile } from '../../../components/virtualizer'; @@ -38,7 +39,6 @@ import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; import { useCategoryHandler } from '../../../hooks/useCategoryHandler'; import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; import { useDirectRooms } from './useDirectRooms'; -import { openInviteUser } from '../../../../client/action/navigation'; import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page'; import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories'; import { useRoomsUnread } from '../../../state/hooks/unread'; @@ -50,6 +50,7 @@ import { getRoomNotificationMode, useRoomsNotificationPreferencesContext, } from '../../../hooks/useRoomsNotificationPreferences'; +import { useDirectCreateSelected } from '../../../hooks/router/useDirectSelected'; type DirectMenuProps = { requestClose: () => void; @@ -138,6 +139,8 @@ function DirectHeader() { } function DirectEmpty() { + const navigate = useNavigate(); + return ( } options={ -