fix(media-gallery): dock as a flex sibling like MembersDrawer (was floating)
The real reason the gallery didn't look or function like the Members drawer
or Saved Messages: it was a position:fixed overlay floating over the timeline,
mounted from RoomViewHeader. Now it docks into the room layout row exactly like
MembersDrawer.
- new mediaGalleryAtom (mirrors bookmarksPanelAtom) holds the open state
- RoomViewHeader toggles the atom instead of local useState and no longer
renders the panel
- Room.tsx renders <MediaGallery> as a flex sibling of the timeline with a
vertical Line separator on desktop and key={room.roomId} to reset per room
- MediaGallery.css: static width on desktop, position:fixed inset:0 full-screen
only on mobile (identical strategy to MembersDrawer.css); root Box shrink="No"
The panel now shares the row with the timeline instead of overlapping it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -308,7 +308,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
### 🟠 Moderate — Interaction Pattern or Visual Deviations
|
||||
|
||||
| # | Area | File | Lines | Issue | Native Pattern |
|
||||
| :-- | :------------------------- | :---------------------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| :-- | :------------------------- | :---------------------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| N5 | Read Receipts | `ReadReceiptAvatars.tsx` | 62–137 | Trigger button is raw `<button>` with `onMouseEnter`/`onMouseLeave` JS style mutation for hover state | All interactive elements use `useHover` from `react-aria` and folds variant system for hover; direct `.style` mutation used nowhere else on buttons |
|
||||
| N6 | Read Receipts | `ReadReceiptAvatars.tsx` & `Message.tsx` | 32–56 / 268–283 | Two code paths open `EventReaders`: avatar-pill path uses `useModalStyle(360)` for mobile fullscreen; context-menu path (`MessageReadReceiptItem`) does not — on mobile the context menu path opens a fixed-size non-fullscreen modal for the same content | All modals that share a layout variant use `useModalStyle` consistently; `MessageReadReceiptItem` was not updated when `useModalStyle` was added |
|
||||
| N7 | Delivery Status | `Message.tsx` | 89–148 | `DeliveryStatus` renders Unicode glyphs (`⟳ ✓ ✕`) in a `<span>` with `fontSize: '10px'` instead of folds `<Icon>` components — **FIXED**: replaced with `Icons.Check/Cross/Send` via `<Icon size="100">` | `Icons.Check`, `Icons.Cross`, etc. are used for all other status glyphs; folds `Text` size tokens for all supplementary text |
|
||||
@@ -316,7 +316,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
| N9 | GIF Button | `RoomInput.tsx` | 1076–1087 | GIF toolbar button renders `<Text size="T200">` with hand-rolled `fontWeight`/`fontSize`/`letterSpacing` instead of `<Icon>` — **WON'T FIX (deliberate)**: folds has no GIF icon, and "GIF" is a widely-recognized text affordance (Slack/Discord/Element all use a text label). Converting to an arbitrary icon would be less clear, not more. | All 8 other toolbar buttons (`Smile`, `Sticker`, `Location`, `Poll`, etc.) use `<Icon src={...} />` exclusively |
|
||||
| N10 | Send Animation | `Message.tsx` + `Animations.css.ts` | 979–998 / 60–71 | `MsgAppearClass` and `MentionHighlightPulse` both animate `transform: scale` on the same `MessageBase` DOM node — on self-sent mention messages both classes apply simultaneously and fight over the `transform` property | Pre-existing `highlightAnime` only animates `backgroundColor`; no prior `transform` animation on `MessageBase` |
|
||||
| N11 | AvatarDecoration | `AvatarDecoration.tsx` | 5 / 38–41 | Fixed 8px inset on all sides regardless of avatar size — at folds size `"200"` (~32px) the decoration bleeds 50% of the avatar diameter, clipping against `overflow: hidden` parent containers in member lists. **Inset issue still OPEN.** _Related regression fixed in `useAvatarDecoration.ts`_: the decoration fetch cached **all** failures (including transient 429/5xx) as "no decoration" permanently for the session, so a single rate-limited burst (member list / timeline mount many avatars at once) would make decorations vanish until a full reload. Now only a genuine 404 is cached; transient errors retry on the next mount. | Folds `Avatar` and `PresenceRingAvatar` do not emit overflow outside their bounding box |
|
||||
| N12 | MediaGallery Drawer | `MediaGallery.tsx` | 651–661 | Drawer uses `position: 'fixed'` with hardcoded `width: '320px'` as inline styles on a `<Box>` — **FIXED**: moved positioning/width into co-located `MediaGallery.css.ts` using `toRem(320)` + a `max-width: 750px` full-screen media query (mirrors `MembersDrawer`); border/header now use `config.borderWidth`/`config.space` tokens. Added Escape-to-close on the panel (previously only the lightbox handled Escape). **Full chrome redesign (round 2)** to match native conventions: panel + header switched from `Surface` to `Background` variant (matching `MembersDrawer`/Saved Messages); header now `Text size="H4"` + plain close `IconButton` (dropped the bespoke tooltip-wrapped button); tabs moved to a bordered toolbar strip with the `variant={active?'Primary':'Secondary'} fill={active?'Solid':'Soft'}` pattern from `PolicyListViewer` and now show per-tab counts; the centered "lines + label" month divider replaced with a left-aligned group label (Cinny group-label pattern); thumbnail tiles moved hover/focus styling to CSS `:hover`/`:focus-visible` (no JS hover state) and into `MediaGallery.css.ts`; file rows + grid tokenized. | `MembersDrawer` uses a vanilla-extract class with `width: toRem(266)` and is placed by the layout system, not `position: fixed`. 54px width discrepancy also breaks visual rhythm if both panels could be open |
|
||||
| N12 | MediaGallery Drawer | `MediaGallery.tsx` | 651–661 | Drawer uses `position: 'fixed'` with hardcoded `width: '320px'` as inline styles on a `<Box>` — **FIXED**: moved positioning/width into co-located `MediaGallery.css.ts` using `toRem(320)` + a `max-width: 750px` full-screen media query (mirrors `MembersDrawer`); border/header now use `config.borderWidth`/`config.space` tokens. Added Escape-to-close on the panel (previously only the lightbox handled Escape). **Full chrome redesign (round 2)** to match native conventions: panel + header switched from `Surface` to `Background` variant (matching `MembersDrawer`/Saved Messages); header now `Text size="H4"` + plain close `IconButton` (dropped the bespoke tooltip-wrapped button); tabs moved to a bordered toolbar strip with the `variant={active?'Primary':'Secondary'} fill={active?'Solid':'Soft'}` pattern from `PolicyListViewer` and now show per-tab counts; the centered "lines + label" month divider replaced with a left-aligned group label (Cinny group-label pattern); thumbnail tiles moved hover/focus styling to CSS `:hover`/`:focus-visible` (no JS hover state) and into `MediaGallery.css.ts`; file rows + grid tokenized. **Docking fix (round 3)** — the core of the finding: the gallery was a `position: fixed` overlay floating over the timeline, mounted from `RoomViewHeader`. It is now a **docked flex sibling** in the room layout row, exactly like `MembersDrawer`: open state lifted to a `mediaGalleryAtom` (mirrors `bookmarksPanelAtom`), rendered in `Room.tsx` with a vertical `Line` separator on desktop and `key={room.roomId}` to reset per room; the CSS is static-width on desktop and only `position: fixed; inset: 0` full-screen on mobile (identical strategy to `MembersDrawer.css`). It now shares the row with the timeline instead of overlapping it. | `MembersDrawer` uses a vanilla-extract class with `width: toRem(266)` and is placed by the layout system, not `position: fixed`. 54px width discrepancy also breaks visual rhythm if both panels could be open |
|
||||
| N13 | ScheduledMessagesTray | `ScheduledMessagesTray.tsx` | 108–126 | Collapsible tray header is `<Box as="button">` with `cursor: 'pointer'` inline style and no folds variant — no hover state, no focus ring — **FIXED**: replaced with folds `<Button variant="Secondary" fill="None" radii="0">` using `before`/`after` icon props (gains design-system hover/focus) | All clickable header/toggle elements in the room view use folds `<Button>` or `<IconButton>` with explicit variants for hover/focus; `<Box as="button">` with no variant is used nowhere else |
|
||||
| N14 | ForwardMessageDialog | `ForwardMessageDialog.tsx` | 137–154 | Dialog uses `<Modal>` but has no `<Header>` component and no close `<IconButton>` — only way to close is clicking outside | Every other modal using `<Modal>` or `<Box role="dialog">` includes a `<Header>` with a close `<IconButton>` in the top-right (EditHistoryModal, LeaveRoomPrompt, ScheduleMessageModal, RemindMeDialog, etc.) |
|
||||
| N15 | ScheduleMessageModal | `ScheduleMessageModal.tsx` | 180–193 | Modal root is `<Box as="form" role="dialog">` with manually assembled `borderRadius: config.radii.R400`/`boxShadow` | `ForwardMessageDialog` uses folds `<Modal size="400">` with `R500` radius; the R400 vs R500 mismatch is visible when both dialogs appear in the same session |
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { color, config, toRem } from 'folds';
|
||||
|
||||
// Right-side drawer that floats over the room view. 320px is wider than the
|
||||
// 266px member/bookmark drawers because it hosts a media grid; on narrow
|
||||
// viewports it expands to fill the screen, matching the app's other drawers.
|
||||
// Right-side drawer DOCKED into the room layout row (a flex sibling of the
|
||||
// timeline), exactly like MembersDrawer — not a floating overlay. 320px is a
|
||||
// little wider than the 266px member/bookmark drawers because it hosts a media
|
||||
// grid. On narrow viewports it becomes a full-screen fixed panel, matching the
|
||||
// app's other drawers.
|
||||
export const MediaGalleryDrawer = style({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: toRem(320),
|
||||
zIndex: 500,
|
||||
overflow: 'hidden',
|
||||
borderLeftWidth: config.borderWidth.B300,
|
||||
borderLeftStyle: 'solid',
|
||||
borderLeftColor: color.Background.ContainerLine,
|
||||
'@media': {
|
||||
'(max-width: 750px)': {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
width: '100%',
|
||||
borderLeftWidth: 0,
|
||||
zIndex: 500,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -625,6 +625,7 @@ export function MediaGallery({ room, onClose }: MediaGalleryProps) {
|
||||
<>
|
||||
<Box
|
||||
className={classNames(css.MediaGalleryDrawer, ContainerColor({ variant: 'Background' }))}
|
||||
shrink="No"
|
||||
direction="Column"
|
||||
>
|
||||
{/* Header */}
|
||||
|
||||
@@ -2,9 +2,11 @@ import React, { useCallback } from 'react';
|
||||
import { Box, Line } from 'folds';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { RoomView } from './RoomView';
|
||||
import { MembersDrawer } from './MembersDrawer';
|
||||
import { MediaGallery } from './MediaGallery';
|
||||
import { mediaGalleryAtom } from '../../state/mediaGallery';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
@@ -31,6 +33,8 @@ export function Room() {
|
||||
const callEmbed = useCallEmbed();
|
||||
|
||||
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const galleryOpen = useAtomValue(mediaGalleryAtom);
|
||||
const setGalleryOpen = useSetAtom(mediaGalleryAtom);
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const powerLevels = usePowerLevels(room);
|
||||
@@ -78,6 +82,14 @@ export function Room() {
|
||||
<CallChatView />
|
||||
</>
|
||||
)}
|
||||
{!callView && galleryOpen && (
|
||||
<>
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
<Line variant="Background" direction="Vertical" size="300" />
|
||||
)}
|
||||
<MediaGallery key={room.roomId} room={room} onClose={() => setGalleryOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
{!callView && isDrawer && (
|
||||
<>
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
|
||||
@@ -73,7 +73,7 @@ import { RoomSettingsPage } from '../../state/roomSettings';
|
||||
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
|
||||
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
import { MediaGallery } from './MediaGallery';
|
||||
import { mediaGalleryAtom } from '../../state/mediaGallery';
|
||||
import { usePendingKnocks } from '../../hooks/usePendingKnocks';
|
||||
import { bookmarksPanelAtom } from '../../state/bookmarksPanel';
|
||||
|
||||
@@ -488,7 +488,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
: undefined;
|
||||
|
||||
const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [galleryOpen, setGalleryOpen] = useState(false);
|
||||
const [galleryOpen, setGalleryOpen] = useAtom(mediaGalleryAtom);
|
||||
const pendingKnocks = usePendingKnocks(room);
|
||||
|
||||
const handleSearchClick = () => {
|
||||
@@ -820,7 +820,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
room={room}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
galleryOpen={galleryOpen}
|
||||
onToggleGallery={() => setGalleryOpen((v) => !v)}
|
||||
onToggleGallery={() => setGalleryOpen((v: boolean) => !v)}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
@@ -828,7 +828,6 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
{galleryOpen && <MediaGallery room={room} onClose={() => setGalleryOpen(false)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const mediaGalleryAtom = atom<boolean>(false);
|
||||
Reference in New Issue
Block a user