diff --git a/src/app/components/RoomNotificationSwitcher.tsx b/src/app/components/RoomNotificationSwitcher.tsx index 9bab99c5..9f02debf 100644 --- a/src/app/components/RoomNotificationSwitcher.tsx +++ b/src/app/components/RoomNotificationSwitcher.tsx @@ -56,8 +56,17 @@ export function RoomNotificationModeSwitcher({ const [menuCords, setMenuCords] = useState(); - const handleOpenMenu: MouseEventHandler = (evt) => { - setMenuCords(evt.currentTarget.getBoundingClientRect()); + const open = !!menuCords; + const handleToggleMenu: MouseEventHandler = (evt) => { + // Second click on the trigger CLOSES the popout instead of re-anchoring (reopening) it. `open` + // is the pre-click render value: even though focus-trap's `clickOutsideDeactivates` also fires + // `onDeactivate` for this same click, both paths resolve to "close" — so there's no reopen race + // regardless of listener order. + if (open) { + setMenuCords(undefined); + } else { + setMenuCords(evt.currentTarget.getBoundingClientRect()); + } }; const handleClose = () => { @@ -117,7 +126,7 @@ export function RoomNotificationModeSwitcher({ } > - {children(handleOpenMenu, !!menuCords, changing)} + {children(handleToggleMenu, open, changing)} ); } diff --git a/src/app/features/bots/AiChatMenu.tsx b/src/app/features/bots/AiChatMenu.tsx index d804df88..907b6648 100644 --- a/src/app/features/bots/AiChatMenu.tsx +++ b/src/app/features/bots/AiChatMenu.tsx @@ -1,21 +1,17 @@ -import React, { forwardRef } from 'react'; -import { Box, Icon, Icons, Line, Menu, MenuItem, Spinner, Text, config, toRem } from 'folds'; +import React, { forwardRef, useState } from 'react'; +import { Icons, Menu, Spinner } 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'; +// Reuse the EXACT row vocabulary + popout chrome of the 1:1 chat's ⋮ menu (RoomActionsMenu) so the +// AI surface's overflow menu is the same popup, styled identically — only the action set differs. +import { ActionRow, ActionSectionLine, RowChevron } from '../room/room-actions/RoomActions'; +import * as css from '../room/room-actions/RoomActions.css'; type AiChatMenuProps = { room: Room; @@ -25,130 +21,81 @@ type AiChatMenuProps = { 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. +// The native AI conversation surface's ⋮ menu. Same popup as the 1:1 chat overflow menu +// (RoomActionsMenu: `Menu variant="Surface"` + ActionRow rows + ActionSectionLine), carrying the +// AI conversation actions (New chat / Chats / Privacy & data) plus the generic room actions +// (notifications / leave). No "Mark as read" — useless here — and no widget "Show chat". export const AiChatMenu = forwardRef( ({ 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(); - }; + const [promptLeave, setPromptLeave] = useState(false); return ( - - - + {promptLeave && ( + setPromptLeave(false)} + /> + )} + +
+ { onNewChat(); requestClose(); }} - size="300" - after={} - radii="300" - > - - {t('Bots.conversations.new_chat')} - - - + { onOpenHistory(); requestClose(); }} - size="300" - after={} - radii="300" - > - - {t('Bots.conversations.title')} - - - } - radii="300" - disabled={!unread} - > - - {t('Room.mark_as_read')} - - + ariaHasPopup + trailing={} + /> {(handleOpen, opened, changing) => ( - - ) : ( - - ) - } - radii="300" - aria-pressed={opened} + - - {t('Room.notifications')} - - + ariaPressed={opened} + ariaHasPopup + trailing={changing ? : } + /> )} - { onOpenPrivacy(); requestClose(); }} - size="300" - after={} - radii="300" - > - - {t('Bots.privacy.menu')} - - - - - - - {(promptLeave, setPromptLeave) => ( - <> - setPromptLeave(true)} - variant="Critical" - fill="None" - size="300" - after={} - radii="300" - aria-pressed={promptLeave} - > - - {t('Room.leave_room')} - - - {promptLeave && ( - setPromptLeave(false)} - /> - )} - - )} - - + ariaHasPopup + trailing={} + /> +
+ + +
+ setPromptLeave(true)} + accent="critical" + ariaPressed={promptLeave} + /> +
); } diff --git a/src/app/features/bots/BotConversations.css.ts b/src/app/features/bots/BotConversations.css.ts index 794b18dd..3c487325 100644 --- a/src/app/features/bots/BotConversations.css.ts +++ b/src/app/features/bots/BotConversations.css.ts @@ -1,6 +1,7 @@ -import { style } from '@vanilla-extract/css'; +import { globalStyle, style } from '@vanilla-extract/css'; import { color, config, toRem } from 'folds'; -import { VOJO_HORSESHOE_GAP_PX } from '../../styles/horseshoe'; +import { VOJO_HORSESHOE_GAP_PX, VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe'; +import { ChatComposer } from '../room/RoomView.css'; // Bridge widgets centre their body content in a 960px column (apps/widget-telegram/src/styles.css // `.app { max-width: 960px; margin: 0 auto }`, mirrored by the host hero's `BotShell.HeroInner`). @@ -53,6 +54,13 @@ export const Main = style({ flexDirection: 'column', }); +// Hide the composer/input form entirely while the chats list (History panel) is open; the chat +// messages stay visible. `ChatComposer` marks both the new-chat composer and the in-thread +// composer, so both are removed. +globalStyle(`${Main}[data-history-open] ${ChatComposer}`, { + display: 'none', +}); + // Chat-history panel, anchored to the RIGHT edge of the body, slid off-screen until toggled. Sits // above the Main content (zIndex 2) over a click-to-close Backdrop (zIndex 1). Raised // Surface.Container tone + a soft cast shadow lift it off the surface so the open/close reads as a @@ -62,28 +70,30 @@ export const History = style({ insetBlock: 0, right: 0, zIndex: 2, - width: '50%', - maxWidth: toRem(440), - minWidth: toRem(300), + // ~1.1× narrower than before (440→400 / 300→280 / 50%→45%). + width: '45%', + maxWidth: toRem(400), + minWidth: toRem(280), display: 'flex', flexDirection: 'column', + // Composer/input-form tone (Surface.Container = #0d0e11 — the same fill the message input uses). backgroundColor: color.Surface.Container, - borderLeft: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`, + // No border — the panel separates from the chat via its own cast shadow + the rounded corner. + // Round the inner (top-left) corner with the app-wide horseshoe radius, matching the Direct-tab + // chat list (PageNav content) and the composer card. + borderTopLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX), transform: 'translateX(100%)', transition: 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1)', selectors: { - // Shadow ONLY when open. The base `translateX(100%)` parks the panel off the right edge, but its - // cast shadow (offset -12px, i.e. leftward, + 40px blur) reaches back INTO the body and Body's - // `overflow: hidden` can't clip it (the leak lands inside the box, not past its edge) — so a base - // shadow paints a dark sliver on the right edge even while the drawer is closed. + // No cast shadow — the panel separates from the chat via its darker bg + rounded corner only. '&[data-open]': { transform: 'translateX(0)', - boxShadow: '-12px 0 40px rgba(0, 0, 0, 0.32)', }, }, '@media': { '(prefers-reduced-motion: reduce)': { transition: 'none' }, - '(max-width: 600px)': { width: '86%', maxWidth: 'none' }, + // Narrower on native so there's a bigger gap on the left of the chats list. + '(max-width: 600px)': { width: '78%', maxWidth: 'none' }, }, }); @@ -150,9 +160,8 @@ export const Backdrop = style({ margin: 0, appearance: 'none', cursor: 'default', - // Gentler scrim than the stock 0.4 — the panel's own shadow already separates it from the - // surface, so the chat behind only needs a light dim, not a heavy black-out. - backgroundColor: 'rgba(0, 0, 0, 0.28)', + // No scrim — the chat behind must not dim when the chats list opens (click-to-close only). + backgroundColor: 'transparent', }); // History row: a rounded card holding the conversation title and a muted last-activity time. diff --git a/src/app/features/bots/BotConversations.tsx b/src/app/features/bots/BotConversations.tsx index afce0d4c..92c81e81 100644 --- a/src/app/features/bots/BotConversations.tsx +++ b/src/app/features/bots/BotConversations.tsx @@ -193,7 +193,7 @@ export function BotConversations({ preset, room, rootId }: BotConversationsProps -
+
{rootId ? ( ( const canInvite = permissions.action('invite', mx.getSafeUserId()); const notificationPreferences = useRoomsNotificationPreferencesContext(); const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId); - const notificationModeLabel = useNotificationModeLabel(notificationMode); const pinnedEvents = useRoomPinnedEvents(room); const { navigateRoom } = useRoomNavigate(); const openSettings = useOpenRoomSettings(); @@ -148,16 +140,7 @@ export const RoomActionsMenu = forwardRef( onClick={handleOpen} ariaPressed={opened} ariaHasPopup - trailing={ - changing ? ( - - ) : ( - <> - {notificationModeLabel} - - - ) - } + trailing={changing ? : } /> )}