Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/app/components/direct-invite-prompt/DirectInvitePrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
Button,
Dialog,
Header,
Icon,
IconButton,
Icons,
Overlay,
OverlayBackdrop,
OverlayCenter,
Spinner,
Text,
color,
config,
} from 'folds';
import { stopPropagation } from '../../utils/keyboard';

type DirectInvitePromptProps = {
onCancel: () => void;
onInviteDirect: () => void;
onConvertAndInvite: () => void;
converting: boolean;
convertError?: string;
};

export function DirectInvitePrompt({
onCancel,
onInviteDirect,
onConvertAndInvite,
converting,
convertError,
}: DirectInvitePromptProps) {
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: onCancel,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Invite another Member</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="200">
<Text size="T300">
This is a <b>Direct Message</b> room, intended for a conversation between two
persons. Would you like to convert it into a <b>group chat</b> before continuing?
</Text>
{convertError && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to convert direct message to room! {convertError}
</Text>
)}
</Box>
<Box direction="Column" gap="200">
<Button
variant="Primary"
onClick={onConvertAndInvite}
disabled={converting}
before={
converting ? <Spinner fill="Solid" variant="Primary" size="200" /> : undefined
}
aria-disabled={converting}
>
<Text size="B400">{converting ? 'Converting...' : 'Convert to Group Chat and Invite'}</Text>
</Button>
<Button variant="Warning" fill="Soft" onClick={onInviteDirect} disabled={converting}>
<Text size="B400">Invite to Direct Message anyway</Text>
</Button>
<Button variant="Secondary" fill="Soft" onClick={onCancel} disabled={converting}>
<Text size="B400">Cancel</Text>
</Button>
</Box>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}
1 change: 1 addition & 0 deletions src/app/components/direct-invite-prompt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DirectInvitePrompt';
4 changes: 3 additions & 1 deletion src/app/components/info-card/InfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type InfoCardProps = {
title?: ReactNode;
description?: ReactNode;
before?: ReactNode;
beforeAlign?: 'Start' | 'Center';
after?: ReactNode;
children?: ReactNode;
};
Expand All @@ -18,6 +19,7 @@ export function InfoCard({
title,
description,
before,
beforeAlign = 'Start',
after,
children,
}: InfoCardProps) {
Expand All @@ -29,7 +31,7 @@ export function InfoCard({
>
<Box gap="200" alignItems="Center">
{before && (
<Box shrink="No" alignSelf="Start">
<Box shrink="No" alignSelf={beforeAlign}>
{before}
</Box>
)}
Expand Down
85 changes: 76 additions & 9 deletions src/app/components/room-intro/RoomIntro.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import React, { useCallback, useState } from 'react';
import { Avatar, Box, Button, Spinner, Text, as } from 'folds';
import React, { useCallback, useEffect, useState } from 'react';
import {
Avatar,
Box,
Button,
Icon,
Icons,
Spinner,
Text,
as,
} from 'folds';
import { Room } from 'matrix-js-sdk';
import { useAtomValue } from 'jotai';
import { IRoomCreateContent, Membership, StateEvent } from '../../../types/matrix/room';
import { getMemberDisplayName, getStateEvent } from '../../utils/room';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
import { getMxIdLocalPart, mxcUrlToHttp, removeRoomIdFromMDirect } from '../../utils/matrix';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { timeDayMonthYear, timeHourMinute } from '../../utils/time';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
Expand All @@ -17,6 +26,8 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { InviteUserPrompt } from '../invite-user-prompt';
import { InfoCard } from '../info-card';
import { DirectInvitePrompt } from '../direct-invite-prompt';

export type RoomIntroProps = {
room: Room;
Expand All @@ -27,7 +38,9 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
const useAuthentication = useMediaAuthentication();
const { navigateRoom } = useRoomNavigate();
const mDirects = useAtomValue(mDirectAtom);
const isDirectConversation = mDirects.has(room.roomId);
const [invitePrompt, setInvitePrompt] = useState(false);
const [directInvitePrompt, setDirectInvitePrompt] = useState(false);

const createEvent = getStateEvent(room, StateEvent.RoomCreate);
const avatarMxc = useRoomAvatar(room, mDirects.has(room.roomId));
Expand All @@ -48,6 +61,37 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>

const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');

const handleInvite = () => {
if (isDirectConversation) {
setDirectInvitePrompt(true);
return;
}
setInvitePrompt(true);
};

const handleInviteDirect = () => {
setDirectInvitePrompt(false);
setInvitePrompt(true);
};

const [convertState, convertToRoom] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
await removeRoomIdFromMDirect(mx, room.roomId);
}, [mx, room.roomId])
);

const handleConvertAndInvite = () => {
if (convertState.status === AsyncStatus.Loading) return;
convertToRoom().catch(() => {});
};

useEffect(() => {
if (convertState.status === AsyncStatus.Success) {
setDirectInvitePrompt(false);
setInvitePrompt(true);
}
}, [convertState.status]);

return (
<Box direction="Column" grow="Yes" gap="500" {...props} ref={ref}>
<Box>
Expand Down Expand Up @@ -75,14 +119,25 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
{` on ${timeDayMonthYear(ts)} ${timeHourMinute(ts, hour24Clock)}`}
</Text>
)}
{isDirectConversation && (
<InfoCard
variant="Primary"
before={<Icon size="100" src={Icons.User} />}
beforeAlign="Center"
description="This is a direct message"
after={
<Button onClick={handleInvite} variant="Secondary" size="300" radii="300">
<Text size="B300">Invite another Member</Text>
</Button>
}
/>
)}
</Box>
<Box gap="200" wrap="Wrap">
<Button onClick={() => setInvitePrompt(true)} variant="Secondary" size="300" radii="300">
<Text size="B300">Invite Member</Text>
</Button>

{invitePrompt && (
<InviteUserPrompt room={room} requestClose={() => setInvitePrompt(false)} />
{!isDirectConversation && (
<Button onClick={handleInvite} variant="Secondary" size="300" radii="300">
<Text size="B300">Invite another Member</Text>
</Button>
)}
{typeof prevRoomId === 'string' &&
(mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (
Expand Down Expand Up @@ -113,6 +168,18 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
</Button>
))}
</Box>
{invitePrompt && <InviteUserPrompt room={room} requestClose={() => setInvitePrompt(false)} />}
{directInvitePrompt && (
<DirectInvitePrompt
onCancel={() => setDirectInvitePrompt(false)}
onInviteDirect={handleInviteDirect}
onConvertAndInvite={handleConvertAndInvite}
converting={convertState.status === AsyncStatus.Loading}
convertError={
convertState.status === AsyncStatus.Error ? convertState.error.message : undefined
}
/>
)}
</Box>
</Box>
);
Expand Down
55 changes: 53 additions & 2 deletions src/app/features/room/RoomViewHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { MouseEventHandler, forwardRef, useState } from 'react';
import React, { MouseEventHandler, forwardRef, useCallback, useEffect, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
Expand Down Expand Up @@ -38,7 +38,12 @@ import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { getHomeSearchPath, getSpaceSearchPath, withSearchParam } from '../../pages/pathUtils';
import { getCanonicalAliasOrRoomId, isRoomAlias, mxcUrlToHttp } from '../../utils/matrix';
import {
getCanonicalAliasOrRoomId,
isRoomAlias,
mxcUrlToHttp,
removeRoomIdFromMDirect,
} from '../../utils/matrix';
import { _SearchPathSearchParams } from '../../pages/paths';
import * as css from './RoomViewHeader.css';
import { useRoomUnread } from '../../state/hooks/unread';
Expand Down Expand Up @@ -69,6 +74,8 @@ import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { DirectInvitePrompt } from '../../components/direct-invite-prompt';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';

type RoomMenuProps = {
room: Room;
Expand All @@ -83,21 +90,51 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose

const permissions = useRoomPermissions(creators, powerLevels);
const canInvite = permissions.action('invite', mx.getSafeUserId());
const mDirects = useAtomValue(mDirectAtom);
const isDirectConversation = mDirects.has(room.roomId);
const notificationPreferences = useRoomsNotificationPreferencesContext();
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
const { navigateRoom } = useRoomNavigate();

const [invitePrompt, setInvitePrompt] = useState(false);
const [directInvitePrompt, setDirectInvitePrompt] = useState(false);

const handleMarkAsRead = () => {
markAsRead(mx, room.roomId, hideActivity);
requestClose();
};

const handleInvite = () => {
if (isDirectConversation) {
setDirectInvitePrompt(true);
return;
}
setInvitePrompt(true);
};

const handleInviteDirect = () => {
setDirectInvitePrompt(false);
setInvitePrompt(true);
};

const [convertState, convertToRoom] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
await removeRoomIdFromMDirect(mx, room.roomId);
}, [mx, room.roomId])
);

const handleConvertAndInvite = () => {
if (convertState.status === AsyncStatus.Loading) return;
convertToRoom().catch(() => {});
};

useEffect(() => {
if (convertState.status === AsyncStatus.Success) {
setDirectInvitePrompt(false);
setInvitePrompt(true);
}
}, [convertState.status]);

const handleCopyLink = () => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
Expand All @@ -123,6 +160,20 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(({ room, requestClose
}}
/>
)}
{directInvitePrompt && (
<DirectInvitePrompt
onCancel={() => {
setDirectInvitePrompt(false);
requestClose();
}}
onInviteDirect={handleInviteDirect}
onConvertAndInvite={handleConvertAndInvite}
converting={convertState.status === AsyncStatus.Loading}
convertError={
convertState.status === AsyncStatus.Error ? convertState.error.message : undefined
}
/>
)}
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem
onClick={handleMarkAsRead}
Expand Down