vojo/src/app/features/bots/AiChatMenu.tsx

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>
);
}
);