155 lines
5.8 KiB
TypeScript
155 lines
5.8 KiB
TypeScript
import React, { forwardRef } from 'react';
|
|
import { Box, Icon, Icons, Line, Menu, MenuItem, Spinner, Text, config, toRem } from 'folds';
|
|
import type { Room } from 'matrix-js-sdk';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { useSetting } from '../../state/hooks/settings';
|
|
import { settingsAtom } from '../../state/settings';
|
|
import { useRoomUnread } from '../../state/hooks/unread';
|
|
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
|
import {
|
|
getRoomNotificationMode,
|
|
getRoomNotificationModeIcon,
|
|
useRoomsNotificationPreferencesContext,
|
|
} from '../../hooks/useRoomsNotificationPreferences';
|
|
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
|
|
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
|
import { markAsRead } from '../../utils/notifications';
|
|
|
|
type AiChatMenuProps = {
|
|
room: Room;
|
|
requestClose: () => void;
|
|
onNewChat: () => void;
|
|
onOpenHistory: () => void;
|
|
onOpenPrivacy: () => void;
|
|
};
|
|
|
|
// The native AI conversation surface's ⋮ menu. Deliberately NOT BotShellMenu: there is no widget
|
|
// here, so there is no "Show chat" toggle (it would be meaningless). It carries the conversation
|
|
// actions (New chat / History / Privacy & data) plus the generic room actions (mark read /
|
|
// notifications / leave)
|
|
// that apply to any DM — reusing the same shared switcher / leave-prompt primitives so behaviour
|
|
// can't drift from the rest of the app.
|
|
export const AiChatMenu = forwardRef<HTMLDivElement, AiChatMenuProps>(
|
|
({ room, requestClose, onNewChat, onOpenHistory, onOpenPrivacy }, ref) => {
|
|
const { t } = useTranslation();
|
|
const mx = useMatrixClient();
|
|
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
|
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
|
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
|
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
|
|
|
|
const handleMarkAsRead = () => {
|
|
markAsRead(mx, room.roomId, hideActivity);
|
|
requestClose();
|
|
};
|
|
|
|
return (
|
|
<Menu ref={ref} style={{ maxWidth: toRem(264), width: '100vw' }}>
|
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
|
<MenuItem
|
|
onClick={() => {
|
|
onNewChat();
|
|
requestClose();
|
|
}}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.Plus} />}
|
|
radii="300"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
{t('Bots.conversations.new_chat')}
|
|
</Text>
|
|
</MenuItem>
|
|
<MenuItem
|
|
onClick={() => {
|
|
onOpenHistory();
|
|
requestClose();
|
|
}}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.RecentClock} />}
|
|
radii="300"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
{t('Bots.conversations.title')}
|
|
</Text>
|
|
</MenuItem>
|
|
<MenuItem
|
|
onClick={handleMarkAsRead}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.CheckTwice} />}
|
|
radii="300"
|
|
disabled={!unread}
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
{t('Room.mark_as_read')}
|
|
</Text>
|
|
</MenuItem>
|
|
<RoomNotificationModeSwitcher roomId={room.roomId} value={notificationMode}>
|
|
{(handleOpen, opened, changing) => (
|
|
<MenuItem
|
|
size="300"
|
|
after={
|
|
changing ? (
|
|
<Spinner size="100" variant="Secondary" />
|
|
) : (
|
|
<Icon size="100" src={getRoomNotificationModeIcon(notificationMode)} />
|
|
)
|
|
}
|
|
radii="300"
|
|
aria-pressed={opened}
|
|
onClick={handleOpen}
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
{t('Room.notifications')}
|
|
</Text>
|
|
</MenuItem>
|
|
)}
|
|
</RoomNotificationModeSwitcher>
|
|
<MenuItem
|
|
onClick={() => {
|
|
onOpenPrivacy();
|
|
requestClose();
|
|
}}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.ShieldLock} />}
|
|
radii="300"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
{t('Bots.privacy.menu')}
|
|
</Text>
|
|
</MenuItem>
|
|
</Box>
|
|
<Line variant="Surface" size="300" />
|
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
|
<UseStateProvider initial={false}>
|
|
{(promptLeave, setPromptLeave) => (
|
|
<>
|
|
<MenuItem
|
|
onClick={() => setPromptLeave(true)}
|
|
variant="Critical"
|
|
fill="None"
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.ArrowGoLeft} />}
|
|
radii="300"
|
|
aria-pressed={promptLeave}
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
{t('Room.leave_room')}
|
|
</Text>
|
|
</MenuItem>
|
|
{promptLeave && (
|
|
<LeaveRoomPrompt
|
|
roomId={room.roomId}
|
|
onDone={requestClose}
|
|
onCancel={() => setPromptLeave(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</UseStateProvider>
|
|
</Box>
|
|
</Menu>
|
|
);
|
|
}
|
|
);
|