fix(ui): resolve 29 native UI/UX inconsistencies from folds design audit
Fixes N1–N94 findings from LOTUS_BUGS.md audit pass. Key changes: - ProfileDecoration: raw <button> → folds <Button> for save/remove; remove undefined --accent-cyan var - UserRoomProfile: textarea border uses color.SurfaceVariant.ContainerLine and config tokens instead of undefined --border-interactive var - LotusToastContainer: z-index raised from 9997 → 10001 so toasts appear above Night Light overlay (9998) and modals (9999) - Message.tsx: DeliveryStatus replaces Unicode glyphs with Icon components; MessageQuickReactions returns null instead of <span />; forward menu item gets correct size="100" on after icon - AudioContent: speed chip variant/radii now matches Play chip (Secondary/300) - ReadReceiptAvatars: pill border/radius/padding → folds config tokens; remove dead receipt-pill-btn className - EventReaders: Header size 600→500; close button gets radii="300"; borderBottom shorthand → borderBottomWidth token; remove raw fontSize - General.tsx: selected background/seasonal picker border uses color.Primary.Main instead of color.Critical.Main (error red) - RoomInsights: SectionHeader drops textTransform/letterSpacing/opacity; chart borderRadius → config tokens; remove raw fontSize:9; warning banner → SequenceCard - RoomProfile.tsx: formatting toolbar raw <button> → folds <Button>; topic read-mode renders formatted_body via sanitizeCustomHtml - MsgTypeRenderers: location Open button Chip→Button; opacity:0.65→priority - UploadCardRenderer: caption raw <input> → folds <Input> - VoiceMessageRecorder: replace undefined --bg-surface-variant/--tc-* vars with color.* tokens; replace bare <audio controls> with IconButton play/pause toggle - App.tsx: mention highlight uses WCAG 2.1 relative luminance (gamma linearization) instead of simplified approximation; border now rgba semi-transparent instead of same color as background - RoomNavItem: Mute MenuItem icon moved to before prop - SearchFilters: HasLink chip variant="Success" outlined to match filter bar - RoomViewHeader: Server Notice chip radii Pill→300; fix jotai import order - Fix ESLint import/order errors in DeviceVerificationSetup, RoomTopicViewer, MediaGallery, and RoomViewHeader Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
Spinner,
|
||||
Button,
|
||||
} from 'folds';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||
@@ -74,7 +75,6 @@ import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
import { MediaGallery } from './MediaGallery';
|
||||
import { usePendingKnocks } from '../../hooks/usePendingKnocks';
|
||||
import { useAtom } from 'jotai';
|
||||
import { bookmarksPanelAtom } from '../../state/bookmarksPanel';
|
||||
|
||||
type RoomMenuProps = {
|
||||
@@ -85,247 +85,247 @@ type RoomMenuProps = {
|
||||
};
|
||||
const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
|
||||
({ room, requestClose, galleryOpen, onToggleGallery }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const creators = useRoomCreators(room);
|
||||
const mx = useMatrixClient();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const creators = useRoomCreators(room);
|
||||
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
||||
const isServerNotice = room.getType() === 'm.server_notice';
|
||||
const isCreator = creators.has(mx.getSafeUserId());
|
||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
||||
const isServerNotice = room.getType() === 'm.server_notice';
|
||||
const isCreator = creators.has(mx.getSafeUserId());
|
||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
||||
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||
const [reportRoomOpen, setReportRoomOpen] = useState(false);
|
||||
const [bookmarksOpen, setBookmarksOpen] = useAtom(bookmarksPanelAtom);
|
||||
const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [invitePrompt, setInvitePrompt] = useState(false);
|
||||
const [reportRoomOpen, setReportRoomOpen] = useState(false);
|
||||
const [bookmarksOpen, setBookmarksOpen] = useAtom(bookmarksPanelAtom);
|
||||
const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
markAsRead(mx, room.roomId, hideActivity);
|
||||
requestClose();
|
||||
};
|
||||
const handleMarkAsRead = () => {
|
||||
markAsRead(mx, room.roomId, hideActivity);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
const handleInvite = () => {
|
||||
setInvitePrompt(true);
|
||||
};
|
||||
const handleInvite = () => {
|
||||
setInvitePrompt(true);
|
||||
};
|
||||
|
||||
const openSettings = useOpenRoomSettings();
|
||||
const parentSpace = useSpaceOptionally();
|
||||
const handleOpenSettings = () => {
|
||||
openSettings(room.roomId, parentSpace?.roomId);
|
||||
requestClose();
|
||||
};
|
||||
const openSettings = useOpenRoomSettings();
|
||||
const parentSpace = useSpaceOptionally();
|
||||
const handleOpenSettings = () => {
|
||||
openSettings(room.roomId, parentSpace?.roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
{invitePrompt && (
|
||||
<InviteUserPrompt
|
||||
room={room}
|
||||
requestClose={() => {
|
||||
setInvitePrompt(false);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{reportRoomOpen && (
|
||||
<ReportRoomModal
|
||||
roomId={room.roomId}
|
||||
onClose={() => {
|
||||
setReportRoomOpen(false);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<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>
|
||||
Mark as Read
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<RoomNotificationModeSwitcher roomId={room.roomId} value={notificationMode}>
|
||||
{(handleOpen, opened, changing) => (
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
{invitePrompt && (
|
||||
<InviteUserPrompt
|
||||
room={room}
|
||||
requestClose={() => {
|
||||
setInvitePrompt(false);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{reportRoomOpen && (
|
||||
<ReportRoomModal
|
||||
roomId={room.roomId}
|
||||
onClose={() => {
|
||||
setReportRoomOpen(false);
|
||||
requestClose();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<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>
|
||||
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>
|
||||
Notifications
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
</RoomNotificationModeSwitcher>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setBookmarksOpen((v) => !v);
|
||||
requestClose();
|
||||
}}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Star} filled={bookmarksOpen} />}
|
||||
radii="300"
|
||||
aria-pressed={bookmarksOpen}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Saved Messages
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setPeopleDrawer(!peopleDrawer);
|
||||
requestClose();
|
||||
}}
|
||||
size="300"
|
||||
after={
|
||||
changing ? (
|
||||
<Spinner size="100" variant="Secondary" />
|
||||
) : (
|
||||
<Icon size="100" src={getRoomNotificationModeIcon(notificationMode)} />
|
||||
)
|
||||
}
|
||||
after={<Icon size="100" src={Icons.User} filled={peopleDrawer} />}
|
||||
radii="300"
|
||||
aria-pressed={opened}
|
||||
onClick={handleOpen}
|
||||
aria-pressed={peopleDrawer}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Notifications
|
||||
Members
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
</RoomNotificationModeSwitcher>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setBookmarksOpen((v) => !v);
|
||||
requestClose();
|
||||
}}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Star} filled={bookmarksOpen} />}
|
||||
radii="300"
|
||||
aria-pressed={bookmarksOpen}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Saved Messages
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setPeopleDrawer(!peopleDrawer);
|
||||
requestClose();
|
||||
}}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.User} filled={peopleDrawer} />}
|
||||
radii="300"
|
||||
aria-pressed={peopleDrawer}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Members
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
{screenSize === ScreenSize.Mobile && onToggleGallery && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onToggleGallery();
|
||||
requestClose();
|
||||
}}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Photo} filled={galleryOpen} />}
|
||||
radii="300"
|
||||
aria-pressed={galleryOpen}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Media Gallery
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isServerNotice && (
|
||||
<MenuItem
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||
radii="300"
|
||||
aria-pressed={invitePrompt}
|
||||
disabled={!canInvite}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Invite
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isServerNotice && (
|
||||
<MenuItem
|
||||
onClick={handleOpenSettings}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Setting} />}
|
||||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Room Settings
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
<UseStateProvider initial={false}>
|
||||
{(promptJump, setPromptJump) => (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => setPromptJump(true)}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.RecentClock} />}
|
||||
radii="300"
|
||||
aria-pressed={promptJump}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Jump to Time
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptJump && (
|
||||
<JumpToTime
|
||||
onSubmit={(eventId) => {
|
||||
setPromptJump(false);
|
||||
navigateRoom(room.roomId, eventId);
|
||||
requestClose();
|
||||
}}
|
||||
onCancel={() => setPromptJump(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{screenSize === ScreenSize.Mobile && onToggleGallery && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onToggleGallery();
|
||||
requestClose();
|
||||
}}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Photo} filled={galleryOpen} />}
|
||||
radii="300"
|
||||
aria-pressed={galleryOpen}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Media Gallery
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{!isServerNotice && !isCreator && (
|
||||
<MenuItem
|
||||
onClick={() => setReportRoomOpen(true)}
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Warning} />}
|
||||
radii="300"
|
||||
aria-pressed={reportRoomOpen}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Report Room
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
<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>
|
||||
Leave Room
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptLeave && (
|
||||
<LeaveRoomPrompt
|
||||
roomId={room.roomId}
|
||||
onDone={requestClose}
|
||||
onCancel={() => setPromptLeave(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{!isServerNotice && (
|
||||
<MenuItem
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||
radii="300"
|
||||
aria-pressed={invitePrompt}
|
||||
disabled={!canInvite}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Invite
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
{!isServerNotice && (
|
||||
<MenuItem
|
||||
onClick={handleOpenSettings}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Setting} />}
|
||||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Room Settings
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
<UseStateProvider initial={false}>
|
||||
{(promptJump, setPromptJump) => (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => setPromptJump(true)}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.RecentClock} />}
|
||||
radii="300"
|
||||
aria-pressed={promptJump}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Jump to Time
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptJump && (
|
||||
<JumpToTime
|
||||
onSubmit={(eventId) => {
|
||||
setPromptJump(false);
|
||||
navigateRoom(room.roomId, eventId);
|
||||
requestClose();
|
||||
}}
|
||||
onCancel={() => setPromptJump(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{!isServerNotice && !isCreator && (
|
||||
<MenuItem
|
||||
onClick={() => setReportRoomOpen(true)}
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Warning} />}
|
||||
radii="300"
|
||||
aria-pressed={reportRoomOpen}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Report Room
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
<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>
|
||||
Leave Room
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptLeave && (
|
||||
<LeaveRoomPrompt
|
||||
roomId={room.roomId}
|
||||
onDone={requestClose}
|
||||
onCancel={() => setPromptLeave(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
type CallMenuProps = {
|
||||
@@ -567,7 +567,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<Chip ref={triggerRef} size="400" variant="Warning" radii="Pill" outlined>
|
||||
<Chip ref={triggerRef} size="400" variant="Warning" radii="300" outlined>
|
||||
<Text size="T200">Server Notice</Text>
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user