mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 22:32:26 +03:00
add new invite prompt
This commit is contained in:
parent
5b1cbb5119
commit
713f7e1ee1
9 changed files with 400 additions and 38 deletions
291
src/app/components/invite-user-prompt/InviteUserPrompt.tsx
Normal file
291
src/app/components/invite-user-prompt/InviteUserPrompt.tsx
Normal file
|
@ -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<HTMLInputElement>(null);
|
||||||
|
const directUsers = useDirectUsers();
|
||||||
|
const [validUserId, setValidUserId] = useState<string>();
|
||||||
|
|
||||||
|
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<void, Error, [string, string | undefined]>(
|
||||||
|
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<HTMLFormElement> = (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<HTMLInputElement> = (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<HTMLInputElement> = (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 (
|
||||||
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
|
<OverlayCenter>
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: () => inputRef.current,
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
onDeactivate: requestClose,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog>
|
||||||
|
<Box grow="Yes" direction="Column">
|
||||||
|
<Header
|
||||||
|
size="500"
|
||||||
|
style={{ padding: `0 ${config.space.S200} 0 ${config.space.S400}` }}
|
||||||
|
>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="H4" truncate>
|
||||||
|
Invite
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box shrink="No">
|
||||||
|
<IconButton size="300" radii="300" onClick={requestClose}>
|
||||||
|
<Icon src={Icons.Cross} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Header>
|
||||||
|
<Box
|
||||||
|
as="form"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
shrink="No"
|
||||||
|
style={{ padding: config.space.S400 }}
|
||||||
|
direction="Column"
|
||||||
|
gap="400"
|
||||||
|
>
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Text size="L400">User ID</Text>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
size="500"
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="@john:server"
|
||||||
|
name="userIdInput"
|
||||||
|
variant="Background"
|
||||||
|
disabled={inviting}
|
||||||
|
autoComplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{result && result.items.length > 0 && (
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: resetSearch,
|
||||||
|
returnFocusOnDeactivate: false,
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
allowOutsideClick: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt),
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box style={{ position: 'relative' }}>
|
||||||
|
<Menu style={{ position: 'absolute', top: 0, zIndex: 1, width: '100%' }}>
|
||||||
|
<Scroll size="300" style={{ maxHeight: toRem(100) }}>
|
||||||
|
<div style={{ padding: config.space.S100 }}>
|
||||||
|
{result.items.map((userId) => {
|
||||||
|
const username = `${getMxIdLocalPart(userId)}`;
|
||||||
|
const userServer = getMxIdServer(userId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={userId}
|
||||||
|
type="button"
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={() => handleUserId(userId)}
|
||||||
|
after={
|
||||||
|
<Text size="T200" truncate>
|
||||||
|
{userServer}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
disabled={inviting}
|
||||||
|
>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
<b>
|
||||||
|
{queryHighlighRegex
|
||||||
|
? highlightText(queryHighlighRegex, [
|
||||||
|
username ?? userId,
|
||||||
|
])
|
||||||
|
: username}
|
||||||
|
</b>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Scroll>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
</FocusTrap>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Text size="L400">Reason (Optional)</Text>
|
||||||
|
<TextArea
|
||||||
|
size="500"
|
||||||
|
name="reasonInput"
|
||||||
|
variant="Background"
|
||||||
|
rows={4}
|
||||||
|
resize="None"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{inviteState.status === AsyncStatus.Error && (
|
||||||
|
<Text size="T200" style={{ color: color.Critical.Main }} className={BreakWord}>
|
||||||
|
<b>{inviteState.error.message}</b>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!validUserId || inviting}
|
||||||
|
before={inviting && <Spinner size="200" variant="Primary" fill="Solid" />}
|
||||||
|
>
|
||||||
|
<Text size="B400">Invite</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
</FocusTrap>
|
||||||
|
</OverlayCenter>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
1
src/app/components/invite-user-prompt/index.ts
Normal file
1
src/app/components/invite-user-prompt/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './InviteUserPrompt';
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
|
import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
|
||||||
import { Room } from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { openInviteUser } from '../../../client/action/navigation';
|
|
||||||
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
|
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
|
||||||
import { getMemberDisplayName, getStateEvent } from '../../utils/room';
|
import { getMemberDisplayName, getStateEvent } from '../../utils/room';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
@ -17,6 +16,7 @@ import { mDirectAtom } from '../../state/mDirectList';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
|
import { InviteUserPrompt } from '../invite-user-prompt';
|
||||||
|
|
||||||
export type RoomIntroProps = {
|
export type RoomIntroProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -27,6 +27,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
const mDirects = useAtomValue(mDirectAtom);
|
const mDirects = useAtomValue(mDirectAtom);
|
||||||
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const createEvent = getStateEvent(room, StateEvent.RoomCreate);
|
const createEvent = getStateEvent(room, StateEvent.RoomCreate);
|
||||||
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
|
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
|
||||||
|
@ -76,14 +77,13 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box gap="200" wrap="Wrap">
|
<Box gap="200" wrap="Wrap">
|
||||||
<Button
|
<Button onClick={() => setInvitePrompt(true)} variant="Secondary" size="300" radii="300">
|
||||||
onClick={() => openInviteUser(room.roomId)}
|
|
||||||
variant="Secondary"
|
|
||||||
size="300"
|
|
||||||
radii="300"
|
|
||||||
>
|
|
||||||
<Text size="B300">Invite Member</Text>
|
<Text size="B300">Invite Member</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{invitePrompt && (
|
||||||
|
<InviteUserPrompt room={room} requestClose={() => setInvitePrompt(false)} />
|
||||||
|
)}
|
||||||
{typeof prevRoomId === 'string' &&
|
{typeof prevRoomId === 'string' &&
|
||||||
(mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (
|
(mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
|
import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
|
||||||
import { openInviteUser } from '../../../client/action/navigation';
|
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||||
|
@ -30,6 +29,7 @@ import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
||||||
import { IPowerLevels } from '../../hooks/usePowerLevels';
|
import { IPowerLevels } from '../../hooks/usePowerLevels';
|
||||||
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
|
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
|
||||||
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
|
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
|
||||||
|
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||||
|
|
||||||
type HierarchyItemWithParent = HierarchyItem & {
|
type HierarchyItemWithParent = HierarchyItem & {
|
||||||
parentId: string;
|
parentId: string;
|
||||||
|
@ -126,24 +126,39 @@ function InviteMenuItem({
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const room = mx.getRoom(item.roomId);
|
||||||
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
openInviteUser(item.roomId);
|
setInvitePrompt(true);
|
||||||
requestClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<>
|
||||||
onClick={handleInvite}
|
<MenuItem
|
||||||
size="300"
|
onClick={handleInvite}
|
||||||
radii="300"
|
size="300"
|
||||||
variant="Primary"
|
radii="300"
|
||||||
fill="None"
|
variant="Primary"
|
||||||
disabled={disabled}
|
fill="None"
|
||||||
>
|
aria-pressed={invitePrompt}
|
||||||
<Text as="span" size="T300" truncate>
|
disabled={disabled || !room}
|
||||||
Invite
|
>
|
||||||
</Text>
|
<Text as="span" size="T300" truncate>
|
||||||
</MenuItem>
|
Invite
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
{invitePrompt && room && (
|
||||||
|
<InviteUserPrompt
|
||||||
|
room={room}
|
||||||
|
requestClose={() => {
|
||||||
|
setInvitePrompt(false);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { RoomAvatar } from '../../components/room-avatar';
|
import { RoomAvatar } from '../../components/room-avatar';
|
||||||
import { nameInitials } from '../../utils/common';
|
import { nameInitials } from '../../utils/common';
|
||||||
import * as css from './LobbyHeader.css';
|
import * as css from './LobbyHeader.css';
|
||||||
import { openInviteUser } from '../../../client/action/navigation';
|
|
||||||
import { IPowerLevels } from '../../hooks/usePowerLevels';
|
import { IPowerLevels } from '../../hooks/usePowerLevels';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||||
|
@ -38,6 +37,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
|
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||||
|
|
||||||
type LobbyMenuProps = {
|
type LobbyMenuProps = {
|
||||||
powerLevels: IPowerLevels;
|
powerLevels: IPowerLevels;
|
||||||
|
@ -53,9 +53,10 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
||||||
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
||||||
const openSpaceSettings = useOpenSpaceSettings();
|
const openSpaceSettings = useOpenSpaceSettings();
|
||||||
|
|
||||||
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
openInviteUser(space.roomId);
|
setInvitePrompt(true);
|
||||||
requestClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRoomSettings = () => {
|
const handleRoomSettings = () => {
|
||||||
|
@ -65,6 +66,15 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||||
|
{invitePrompt && (
|
||||||
|
<InviteUserPrompt
|
||||||
|
room={space}
|
||||||
|
requestClose={() => {
|
||||||
|
setInvitePrompt(false);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleInvite}
|
onClick={handleInvite}
|
||||||
|
@ -73,6 +83,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
|
aria-pressed={invitePrompt}
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
|
|
|
@ -30,7 +30,6 @@ import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
||||||
import { usePowerLevels } from '../../hooks/usePowerLevels';
|
import { usePowerLevels } from '../../hooks/usePowerLevels';
|
||||||
import { copyToClipboard } from '../../utils/dom';
|
import { copyToClipboard } from '../../utils/dom';
|
||||||
import { markAsRead } from '../../../client/action/notifications';
|
import { markAsRead } from '../../../client/action/notifications';
|
||||||
import { openInviteUser } from '../../../client/action/navigation';
|
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||||
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
||||||
|
@ -51,6 +50,7 @@ import {
|
||||||
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
|
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
|
||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
|
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||||
|
|
||||||
type RoomNavItemMenuProps = {
|
type RoomNavItemMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -70,14 +70,15 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||||
const openRoomSettings = useOpenRoomSettings();
|
const openRoomSettings = useOpenRoomSettings();
|
||||||
const space = useSpaceOptionally();
|
const space = useSpaceOptionally();
|
||||||
|
|
||||||
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(mx, room.roomId, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
openInviteUser(room.roomId);
|
setInvitePrompt(true);
|
||||||
requestClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
|
@ -94,6 +95,15 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||||
|
{invitePrompt && room && (
|
||||||
|
<InviteUserPrompt
|
||||||
|
room={room}
|
||||||
|
requestClose={() => {
|
||||||
|
setInvitePrompt(false);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleMarkAsRead}
|
onClick={handleMarkAsRead}
|
||||||
|
@ -137,6 +147,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
|
aria-pressed={invitePrompt}
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
|
|
|
@ -45,7 +45,6 @@ import { useRoomUnread } from '../../state/hooks/unread';
|
||||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||||
import { markAsRead } from '../../../client/action/notifications';
|
import { markAsRead } from '../../../client/action/notifications';
|
||||||
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
||||||
import { openInviteUser } from '../../../client/action/navigation';
|
|
||||||
import { copyToClipboard } from '../../utils/dom';
|
import { copyToClipboard } from '../../utils/dom';
|
||||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
||||||
|
@ -69,6 +68,7 @@ import { JumpToTime } from './jump-to-time';
|
||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
|
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||||
|
|
||||||
type RoomMenuProps = {
|
type RoomMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -87,14 +87,15 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
||||||
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
|
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
||||||
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const handleMarkAsRead = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(mx, room.roomId, hideActivity);
|
||||||
requestClose();
|
requestClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
openInviteUser(room.roomId);
|
setInvitePrompt(true);
|
||||||
requestClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = () => {
|
||||||
|
@ -113,6 +114,15 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||||
|
{invitePrompt && (
|
||||||
|
<InviteUserPrompt
|
||||||
|
room={room}
|
||||||
|
requestClose={() => {
|
||||||
|
setInvitePrompt(false);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleMarkAsRead}
|
onClick={handleMarkAsRead}
|
||||||
|
@ -156,6 +166,7 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
|
aria-pressed={invitePrompt}
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
|
|
|
@ -82,7 +82,6 @@ import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||||
import { markAsRead } from '../../../../client/action/notifications';
|
import { markAsRead } from '../../../../client/action/notifications';
|
||||||
import { copyToClipboard } from '../../../utils/dom';
|
import { copyToClipboard } from '../../../utils/dom';
|
||||||
import { openInviteUser } from '../../../../client/action/navigation';
|
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
import { getMatrixToRoom } from '../../../plugins/matrix-to';
|
||||||
import { getViaServers } from '../../../plugins/via-servers';
|
import { getViaServers } from '../../../plugins/via-servers';
|
||||||
|
@ -93,6 +92,7 @@ import { settingsAtom } from '../../../state/settings';
|
||||||
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
|
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
|
||||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||||
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
||||||
|
import { InviteUserPrompt } from '../../../components/invite-user-prompt';
|
||||||
|
|
||||||
type SpaceMenuProps = {
|
type SpaceMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -111,6 +111,8 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||||
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
||||||
const openSpaceSettings = useOpenSpaceSettings();
|
const openSpaceSettings = useOpenSpaceSettings();
|
||||||
|
|
||||||
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const allChild = useSpaceChildren(
|
const allChild = useSpaceChildren(
|
||||||
allRoomsAtom,
|
allRoomsAtom,
|
||||||
room.roomId,
|
room.roomId,
|
||||||
|
@ -136,8 +138,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
openInviteUser(room.roomId);
|
setInvitePrompt(true);
|
||||||
requestClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRoomSettings = () => {
|
const handleRoomSettings = () => {
|
||||||
|
@ -147,6 +148,15 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||||
|
{invitePrompt && room && (
|
||||||
|
<InviteUserPrompt
|
||||||
|
room={room}
|
||||||
|
requestClose={() => {
|
||||||
|
setInvitePrompt(false);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleMarkAsRead}
|
onClick={handleMarkAsRead}
|
||||||
|
@ -181,6 +191,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
|
aria-pressed={invitePrompt}
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
|
|
|
@ -57,7 +57,6 @@ import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy';
|
||||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||||
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
|
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
|
||||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||||
import { openInviteUser } from '../../../../client/action/navigation';
|
|
||||||
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
|
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
|
||||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||||
import { markAsRead } from '../../../../client/action/notifications';
|
import { markAsRead } from '../../../../client/action/notifications';
|
||||||
|
@ -84,6 +83,7 @@ import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
||||||
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { BreakWord } from '../../../styles/Text.css';
|
import { BreakWord } from '../../../styles/Text.css';
|
||||||
|
import { InviteUserPrompt } from '../../../components/invite-user-prompt';
|
||||||
|
|
||||||
type SpaceMenuProps = {
|
type SpaceMenuProps = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -102,6 +102,8 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
||||||
const openSpaceSettings = useOpenSpaceSettings();
|
const openSpaceSettings = useOpenSpaceSettings();
|
||||||
const { navigateRoom } = useRoomNavigate();
|
const { navigateRoom } = useRoomNavigate();
|
||||||
|
|
||||||
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||||
|
|
||||||
const allChild = useSpaceChildren(
|
const allChild = useSpaceChildren(
|
||||||
allRoomsAtom,
|
allRoomsAtom,
|
||||||
room.roomId,
|
room.roomId,
|
||||||
|
@ -122,8 +124,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = () => {
|
const handleInvite = () => {
|
||||||
openInviteUser(room.roomId);
|
setInvitePrompt(true);
|
||||||
requestClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRoomSettings = () => {
|
const handleRoomSettings = () => {
|
||||||
|
@ -139,6 +140,15 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
||||||
return (
|
return (
|
||||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||||
|
{invitePrompt && room && (
|
||||||
|
<InviteUserPrompt
|
||||||
|
room={room}
|
||||||
|
requestClose={() => {
|
||||||
|
setInvitePrompt(false);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleMarkAsRead}
|
onClick={handleMarkAsRead}
|
||||||
size="300"
|
size="300"
|
||||||
|
@ -160,6 +170,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
|
||||||
size="300"
|
size="300"
|
||||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||||
radii="300"
|
radii="300"
|
||||||
|
aria-pressed={invitePrompt}
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
>
|
>
|
||||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue