From 8dc4c4d0725f09376d65db25689c23ae49ad8d10 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 18 Jun 2026 22:46:19 -0400 Subject: [PATCH] fix(ui): resolve 29 native UI/UX inconsistencies from folds design audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes N1–N94 findings from LOTUS_BUGS.md audit pass. Key changes: - ProfileDecoration: raw ); } diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx index f91315174..4aebd3ef3 100644 --- a/src/app/components/message/content/AudioContent.tsx +++ b/src/app/components/message/content/AudioContent.tsx @@ -182,8 +182,8 @@ export function AudioContent({ {`${playbackSpeed}×`} diff --git a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx index aa5adf931..2029a6cfd 100644 --- a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx +++ b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx @@ -1,6 +1,16 @@ import React, { useState } from 'react'; import { Room } from 'matrix-js-sdk'; -import { Icon, Icons, Modal, Overlay, OverlayBackdrop, OverlayCenter, Text, color } from 'folds'; +import { + Icon, + Icons, + Modal, + Overlay, + OverlayBackdrop, + OverlayCenter, + Text, + color, + config, +} from 'folds'; import FocusTrap from 'focus-trap-react'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useSetting } from '../../state/hooks/settings'; @@ -64,7 +74,6 @@ export function ReadReceiptAvatars({ onClick={() => setOpen(true)} title={tooltipNames} aria-label={tooltipNames} - className="receipt-pill-btn" style={{ background: 'none', border: 'none', @@ -95,10 +104,12 @@ export function ReadReceiptAvatars({ backgroundColor: lotusTerminal ? 'rgba(0,212,255,0.07)' : color.SurfaceVariant.Container, - border: lotusTerminal ? '1px solid rgba(0,212,255,0.30)' : '1px solid transparent', + border: lotusTerminal + ? `${config.borderWidth.B300} solid rgba(0,212,255,0.30)` + : `${config.borderWidth.B300} solid transparent`, boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none', - borderRadius: '999px', - padding: '2px 6px', + borderRadius: config.radii.Pill, + padding: `${config.space.S100} ${config.space.S200}`, gap: '0px', }} > diff --git a/src/app/components/room-topic-viewer/RoomTopicViewer.tsx b/src/app/components/room-topic-viewer/RoomTopicViewer.tsx index cd586e2a1..974b1daed 100644 --- a/src/app/components/room-topic-viewer/RoomTopicViewer.tsx +++ b/src/app/components/room-topic-viewer/RoomTopicViewer.tsx @@ -1,9 +1,9 @@ import React from 'react'; import parse from 'html-react-parser'; import { as, Box, Header, Icon, IconButton, Icons, Modal, Scroll, Text } from 'folds'; -import { useModalStyle } from '../../hooks/useModalStyle'; import classNames from 'classnames'; import Linkify from 'linkify-react'; +import { useModalStyle } from '../../hooks/useModalStyle'; import * as css from './style.css'; import { LINKIFY_OPTS, scaleSystemEmoji } from '../../plugins/react-custom-html-parser'; import { sanitizeCustomHtml } from '../../utils/sanitize'; diff --git a/src/app/components/upload-card/UploadCardRenderer.tsx b/src/app/components/upload-card/UploadCardRenderer.tsx index 95de16949..af2820327 100644 --- a/src/app/components/upload-card/UploadCardRenderer.tsx +++ b/src/app/components/upload-card/UploadCardRenderer.tsx @@ -1,5 +1,17 @@ import React, { ReactNode, useEffect, useRef, useState } from 'react'; -import { Box, Chip, Icon, IconButton, Icons, Switch, Text, color, config, toRem } from 'folds'; +import { + Box, + Chip, + Icon, + IconButton, + Icons, + Input, + Switch, + Text, + color, + config, + toRem, +} from 'folds'; import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard'; import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload'; import { useMatrixClient } from '../../hooks/useMatrixClient'; @@ -353,25 +365,18 @@ export function UploadCardRenderer({ )} {(fileItem.originalFile.type.startsWith('image') || fileItem.originalFile.type.startsWith('video')) && ( - setMetadata(fileItem, { ...metadata, caption: e.target.value })} + onChange={(e: React.ChangeEvent) => + setMetadata(fileItem, { ...metadata, caption: e.target.value }) + } data-caption-input - style={{ - marginTop: '6px', - width: '100%', - background: 'var(--bg-surface-low)', - border: '1px solid var(--bg-surface-border)', - borderRadius: '6px', - padding: '5px 8px', - fontSize: '0.85rem', - color: 'var(--text-primary)', - outline: 'none', - boxSizing: 'border-box', - transition: 'border-color 0.15s, box-shadow 0.15s', - }} + variant="Secondary" + size="300" + radii="300" + style={{ marginTop: config.space.S200, width: '100%' }} /> )} diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index bd769b425..830bde89e 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -89,7 +89,7 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) { return ( {(status) => { - const color = + const deviceColor = status === VerificationStatus.Verified ? 'var(--tc-positive-normal, #5effc4)' : status === VerificationStatus.Unverified @@ -97,7 +97,7 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) { : 'var(--tc-surface-low-contrast)'; return ( - + {device.displayName ?? device.deviceId} @@ -239,7 +239,7 @@ function UserPrivateNotes({ userId }: { userId: string }) { Private Note - + {saving ? 'Saving…' : charsLeft < 100 ? `${charsLeft} left` : ''} @@ -252,12 +252,11 @@ function UserPrivateNotes({ userId }: { userId: string }) { style={{ width: '100%', resize: 'vertical', - background: 'var(--bg-surface-variant)', + background: color.SurfaceVariant.Container, color: 'inherit', - border: '1px solid var(--border-interactive)', - borderRadius: '6px', - padding: '8px 10px', - fontSize: '14px', + border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`, + borderRadius: config.radii.R300, + padding: `${config.space.S200} ${config.space.S300}`, fontFamily: 'inherit', lineHeight: 1.5, boxSizing: 'border-box', diff --git a/src/app/features/common-settings/general/RoomProfile.tsx b/src/app/features/common-settings/general/RoomProfile.tsx index 4de33bb65..71cd53827 100644 --- a/src/app/features/common-settings/general/RoomProfile.tsx +++ b/src/app/features/common-settings/general/RoomProfile.tsx @@ -4,7 +4,6 @@ import { Button, Chip, color, - config, Icon, IconButton, Icons, @@ -18,6 +17,7 @@ import { import React, { FormEventHandler, useCallback, useMemo, useRef, useState } from 'react'; import { useAtomValue } from 'jotai'; import Linkify from 'linkify-react'; +import parse from 'html-react-parser'; import classNames from 'classnames'; import { JoinRule, MatrixError } from 'matrix-js-sdk'; import { EmojiBoard } from '../../../components/emoji-board'; @@ -33,6 +33,7 @@ import { import { mDirectAtom } from '../../../state/mDirectList'; import { BreakWord, LineClamp3 } from '../../../styles/Text.css'; import { LINKIFY_OPTS } from '../../../plugins/react-custom-html-parser'; +import { sanitizeCustomHtml } from '../../../utils/sanitize'; import { RoomAvatar, RoomIcon } from '../../../components/room-avatar'; import { mxcUrlToHttp } from '../../../utils/matrix'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; @@ -84,7 +85,7 @@ function buildTopicContent(topic: string): Record { const formattedBody = topicMarkdownToHtml(topic); // Use HTML-stripped text as the plain topic so the header shows clean text, not raw markdown syntax const plainTopic = formattedBody.replace(/
/g, '\n').replace(/<[^>]+>/g, ''); - // eslint-disable-next-line @typescript-eslint/naming-convention + return { topic: plainTopic, format: 'org.matrix.custom.html', formatted_body: formattedBody }; } @@ -332,30 +333,30 @@ export function RoomProfileEdit({ { label: '`', syntax: '`', placeholder: 'code', title: 'Inline Code' }, ] as const ).map(({ label, syntax, placeholder, title }) => ( - + + {label} + + ))}
)} @@ -456,7 +457,12 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
{topic && ( - {topic.topic} + {topic.format === 'org.matrix.custom.html' && + typeof topic.formatted_body === 'string' ? ( + parse(sanitizeCustomHtml(topic.formatted_body)) + ) : ( + {topic.topic} + )} )}
diff --git a/src/app/features/message-search/SearchFilters.tsx b/src/app/features/message-search/SearchFilters.tsx index 4735168e3..a8c40276b 100644 --- a/src/app/features/message-search/SearchFilters.tsx +++ b/src/app/features/message-search/SearchFilters.tsx @@ -25,8 +25,8 @@ import { Input, Badge, RectCords, + color, } from 'folds'; -import { color } from 'folds'; import { SearchOrderBy } from 'matrix-js-sdk'; import FocusTrap from 'focus-trap-react'; import { useVirtualizer } from '@tanstack/react-virtual'; @@ -374,7 +374,10 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro const searchUser = useDebounce(_searchUser, SEARCH_DEBOUNCE_OPTS); const handleSearchChange: ChangeEventHandler = (evt) => { const value = evt.currentTarget.value.trim(); - if (!value) { resetSearch(); return; } + if (!value) { + resetSearch(); + return; + } searchUser(value); }; @@ -419,14 +422,30 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro > - + From - + - + {users.length === 0 && ( - No match found! + + No match found! + )}
{vItems.map((vItem) => { @@ -450,7 +469,9 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro aria-pressed={selected} before={} > - {name} + + {name} + ); @@ -467,7 +488,14 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro Save )} - @@ -477,7 +505,9 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro } > ) => setMenuAnchor(e.currentTarget.getBoundingClientRect())} + onClick={(e: React.MouseEvent) => + setMenuAnchor(e.currentTarget.getBoundingClientRect()) + } variant="SurfaceVariant" radii="Pill" before={} @@ -529,22 +559,28 @@ function DateRangeButton({ fromTs, toTs, onChange }: DateRangeButtonProps) { Quick pick - {([ - { label: 'Today', days: 0 }, - { label: 'Last week', days: 7 }, - { label: 'Last month', days: 30 }, - { label: 'Last year', days: 365 }, - ] as const).map(({ label: l, days }) => { + {( + [ + { label: 'Today', days: 0 }, + { label: 'Last week', days: 7 }, + { label: 'Last month', days: 30 }, + { label: 'Last year', days: 365 }, + ] as const + ).map(({ label: l, days }) => { const now = Date.now(); - const from = days === 0 - ? new Date().setHours(0, 0, 0, 0) - : now - days * 24 * 60 * 60 * 1000; + const from = + days === 0 + ? new Date().setHours(0, 0, 0, 0) + : now - days * 24 * 60 * 60 * 1000; return ( { onChange(from, now); setMenuAnchor(undefined); }} + onClick={() => { + onChange(from, now); + setMenuAnchor(undefined); + }} > {l} @@ -746,13 +782,11 @@ export function SearchFilters({ ); })} - + } @@ -761,7 +795,10 @@ export function SearchFilters({ { e.stopPropagation(); onContainsUrlChange(undefined); }} + onClick={(e) => { + e.stopPropagation(); + onContainsUrlChange(undefined); + }} /> ) : undefined } diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index b4c02d506..a8d5606e0 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -453,12 +453,12 @@ const RoomNavItemMenu = forwardRef( > } after={} radii="300" aria-pressed={!!muteMenuAnchor} onClick={(e) => setMuteMenuAnchor(e.currentTarget.getBoundingClientRect())} > - Mute diff --git a/src/app/features/room-settings/RoomInsights.tsx b/src/app/features/room-settings/RoomInsights.tsx index 7b616923f..87c005c1b 100644 --- a/src/app/features/room-settings/RoomInsights.tsx +++ b/src/app/features/room-settings/RoomInsights.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { Avatar, Box, Icon, IconButton, Icons, Scroll, Text, color, config } from 'folds'; import { EventType } from 'matrix-js-sdk'; import { Page, PageContent, PageHeader } from '../../components/page'; +import { SequenceCard } from '../../components/sequence-card'; import { useRoom } from '../../hooks/useRoom'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; @@ -23,14 +24,7 @@ function formatDate(ts: number): string { function SectionHeader({ label }: { label: string }) { return ( - + {label} ); @@ -165,31 +159,22 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { {/* ── Disclaimer banner ── */} - - + + - + Based on {stats.totalMessages} locally cached message {stats.totalMessages !== 1 ? 's' : ''} {stats.oldestTs !== null && stats.newestTs !== null && ( - + from {formatDate(stats.oldestTs)} to {formatDate(stats.newestTs)} )} - + {/* ── Summary row ── */} @@ -350,7 +335,7 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { height: 6, width: barWidth, background: color.Primary.Main, - borderRadius: 3, + borderRadius: config.radii.R300, transition: 'width 0.3s ease', flexShrink: 0, }} @@ -432,7 +417,7 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { count > 0 && count === maxHour ? color.Primary.Main : color.SurfaceVariant.Container, - borderRadius: '2px 2px 0 0', + borderRadius: `${config.radii.R300} ${config.radii.R300} 0 0`, transition: 'height 0.2s ease', }} /> @@ -445,7 +430,7 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { {stats.hourBuckets.map((_, h) => ( {h % 6 === 0 ? ( - + {h} ) : null} diff --git a/src/app/features/room/MediaGallery.tsx b/src/app/features/room/MediaGallery.tsx index 6bb489c36..2fdee001d 100644 --- a/src/app/features/room/MediaGallery.tsx +++ b/src/app/features/room/MediaGallery.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useNearViewport } from '../../hooks/useNearViewport'; import { Box, Button, @@ -16,6 +15,7 @@ import { config, } from 'folds'; import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from 'matrix-js-sdk'; +import { useNearViewport } from '../../hooks/useNearViewport'; import { IEncryptedFile, IImageInfo, IThumbnailContent } from '../../../types/matrix/common'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; @@ -373,7 +373,14 @@ function GalleryTile({ const mx = useMatrixClient(); const tileRef = useRef(null); const nearViewport = useNearViewport(tileRef, 300); - const media = useDecryptedMediaUrl(mx, mxcUrl, encInfo, useAuthentication, mimeType, nearViewport); + const media = useDecryptedMediaUrl( + mx, + mxcUrl, + encInfo, + useAuthentication, + mimeType, + nearViewport, + ); const [hovered, setHovered] = useState(false); const relDate = formatRelativeDate(ts); @@ -422,7 +429,13 @@ function GalleryTile({ {body} )} diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 828cbf0c5..2584ee3f0 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -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( ({ 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 ( - - {invitePrompt && ( - { - setInvitePrompt(false); - requestClose(); - }} - /> - )} - {reportRoomOpen && ( - { - setReportRoomOpen(false); - requestClose(); - }} - /> - )} - - } - radii="300" - disabled={!unread} - > - - Mark as Read - - - - {(handleOpen, opened, changing) => ( + return ( + + {invitePrompt && ( + { + setInvitePrompt(false); + requestClose(); + }} + /> + )} + {reportRoomOpen && ( + { + setReportRoomOpen(false); + requestClose(); + }} + /> + )} + + } + radii="300" + disabled={!unread} + > + + Mark as Read + + + + {(handleOpen, opened, changing) => ( + + ) : ( + + ) + } + radii="300" + aria-pressed={opened} + onClick={handleOpen} + > + + Notifications + + + )} + + + + + { + setBookmarksOpen((v) => !v); + requestClose(); + }} + size="300" + after={} + radii="300" + aria-pressed={bookmarksOpen} + > + + Saved Messages + + + {screenSize === ScreenSize.Mobile && ( { + setPeopleDrawer(!peopleDrawer); + requestClose(); + }} size="300" - after={ - changing ? ( - - ) : ( - - ) - } + after={} radii="300" - aria-pressed={opened} - onClick={handleOpen} + aria-pressed={peopleDrawer} > - Notifications + Members )} - - - - - { - setBookmarksOpen((v) => !v); - requestClose(); - }} - size="300" - after={} - radii="300" - aria-pressed={bookmarksOpen} - > - - Saved Messages - - - {screenSize === ScreenSize.Mobile && ( - { - setPeopleDrawer(!peopleDrawer); - requestClose(); - }} - size="300" - after={} - radii="300" - aria-pressed={peopleDrawer} - > - - Members - - - )} - {screenSize === ScreenSize.Mobile && onToggleGallery && ( - { - onToggleGallery(); - requestClose(); - }} - size="300" - after={} - radii="300" - aria-pressed={galleryOpen} - > - - Media Gallery - - - )} - {!isServerNotice && ( - } - radii="300" - aria-pressed={invitePrompt} - disabled={!canInvite} - > - - Invite - - - )} - {!isServerNotice && ( - } - radii="300" - > - - Room Settings - - - )} - - {(promptJump, setPromptJump) => ( - <> - setPromptJump(true)} - size="300" - after={} - radii="300" - aria-pressed={promptJump} - > - - Jump to Time - - - {promptJump && ( - { - setPromptJump(false); - navigateRoom(room.roomId, eventId); - requestClose(); - }} - onCancel={() => setPromptJump(false)} - /> - )} - + {screenSize === ScreenSize.Mobile && onToggleGallery && ( + { + onToggleGallery(); + requestClose(); + }} + size="300" + after={} + radii="300" + aria-pressed={galleryOpen} + > + + Media Gallery + + )} - - - - - {!isServerNotice && !isCreator && ( - setReportRoomOpen(true)} - variant="Critical" - fill="None" - size="300" - after={} - radii="300" - aria-pressed={reportRoomOpen} - > - - Report Room - - - )} - - {(promptLeave, setPromptLeave) => ( - <> - setPromptLeave(true)} - variant="Critical" - fill="None" - size="300" - after={} - radii="300" - aria-pressed={promptLeave} - > - - Leave Room - - - {promptLeave && ( - setPromptLeave(false)} - /> - )} - + {!isServerNotice && ( + } + radii="300" + aria-pressed={invitePrompt} + disabled={!canInvite} + > + + Invite + + )} - - - - ); -}, + {!isServerNotice && ( + } + radii="300" + > + + Room Settings + + + )} + + {(promptJump, setPromptJump) => ( + <> + setPromptJump(true)} + size="300" + after={} + radii="300" + aria-pressed={promptJump} + > + + Jump to Time + + + {promptJump && ( + { + setPromptJump(false); + navigateRoom(room.roomId, eventId); + requestClose(); + }} + onCancel={() => setPromptJump(false)} + /> + )} + + )} + + + + + {!isServerNotice && !isCreator && ( + setReportRoomOpen(true)} + variant="Critical" + fill="None" + size="300" + after={} + radii="300" + aria-pressed={reportRoomOpen} + > + + Report Room + + + )} + + {(promptLeave, setPromptLeave) => ( + <> + setPromptLeave(true)} + variant="Critical" + fill="None" + size="300" + after={} + radii="300" + aria-pressed={promptLeave} + > + + Leave Room + + + {promptLeave && ( + setPromptLeave(false)} + /> + )} + + )} + + + + ); + }, ); type CallMenuProps = { @@ -567,7 +567,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) { } > {(triggerRef) => ( - + Server Notice )} diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 7048ef87a..5f6b0a055 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -7,6 +7,7 @@ import { Header, Icon, IconButton, + IconSrc, Icons, Input, Line, @@ -95,23 +96,20 @@ function DeliveryStatus({ lotusTerminal: boolean; }) { if (status === null) return null; // confirmed by server — read receipts take over - let icon: string; + let iconSrc: IconSrc; let label: string; let colorStyle: string; + const isSending = status === EventStatus.SENDING || status === EventStatus.ENCRYPTING; if (status === EventStatus.NOT_SENT || status === EventStatus.CANCELLED) { - icon = '✕'; + iconSrc = Icons.Cross; label = 'Failed to send'; colorStyle = lotusTerminal ? '#FF3B3B' : color.Critical.Main; - } else if (status === EventStatus.QUEUED) { - icon = '⟳'; - label = 'Queued'; - colorStyle = lotusTerminal ? 'rgba(0,212,255,0.45)' : color.Secondary.Main; - } else if (status === EventStatus.SENDING || status === EventStatus.ENCRYPTING) { - icon = '⟳'; - label = 'Sending...'; + } else if (status === EventStatus.QUEUED || isSending) { + iconSrc = Icons.Send; + label = isSending ? 'Sending...' : 'Queued'; colorStyle = lotusTerminal ? 'rgba(0,212,255,0.60)' : color.Secondary.Main; } else { - icon = '✓'; + iconSrc = Icons.Check; label = 'Sent'; colorStyle = lotusTerminal ? 'rgba(0,212,255,0.70)' : color.Secondary.Main; } @@ -124,7 +122,6 @@ function DeliveryStatus({ display: 'inline-flex', alignItems: 'center', marginTop: '2px', - fontSize: '10px', lineHeight: 1, color: colorStyle, opacity: 0.85, @@ -134,14 +131,8 @@ function DeliveryStatus({ : {}), }} > - - {icon} + + ); @@ -157,7 +148,7 @@ export const MessageQuickReactions = as<'div', MessageQuickReactionsProps>( const mx = useMatrixClient(); const recentEmojis = useRecentEmoji(mx, 3); - if (recentEmojis.length === 0) return ; + if (recentEmojis.length === 0) return null; return ( <> } + after={} radii="300" onClick={() => { setForwardOpen(true); diff --git a/src/app/features/settings/account/ProfileDecoration.tsx b/src/app/features/settings/account/ProfileDecoration.tsx index f5203c734..9c8f470b5 100644 --- a/src/app/features/settings/account/ProfileDecoration.tsx +++ b/src/app/features/settings/account/ProfileDecoration.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Box, Text, Spinner } from 'folds'; +import { Box, Button, Text, Spinner, color } from 'folds'; import { Method } from 'matrix-js-sdk'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; @@ -170,51 +170,36 @@ export function ProfileDecoration() { : 'None'} {selected && ( - + Remove + )} {hasChanges && ( - + {saving ? 'Saving…' : 'Save'} + )} {saveState.status === AsyncStatus.Error && ( - + Failed to save. Try again. )} diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index e07cf3386..ba8b4f619 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -1658,7 +1658,7 @@ function SeasonalBgGrid({ borderRadius: toRem(8), cursor: 'pointer', border: selected - ? `2px solid ${color.Critical.Main}` + ? `2px solid ${color.Primary.Main}` : '2px solid rgba(128,128,128,0.25)', padding: 0, overflow: 'hidden', @@ -1687,7 +1687,7 @@ function SeasonalBgGrid({ )}
- + {opt.label}
@@ -1724,7 +1724,7 @@ function ChatBgGrid() { cursor: 'pointer', border: chatBackground === opt.value - ? `2px solid ${color.Critical.Main}` + ? `2px solid ${color.Primary.Main}` : '2px solid rgba(128,128,128,0.25)', padding: 0, overflow: 'hidden', @@ -1733,7 +1733,7 @@ function ChatBgGrid() { /> {opt.label} diff --git a/src/app/features/toast/LotusToastContainer.tsx b/src/app/features/toast/LotusToastContainer.tsx index 09f6b511f..c97a2568c 100644 --- a/src/app/features/toast/LotusToastContainer.tsx +++ b/src/app/features/toast/LotusToastContainer.tsx @@ -194,7 +194,7 @@ export function LotusToastContainer() { position: 'fixed', bottom: '1.5rem', right: '1.5rem', - zIndex: 9997, + zIndex: 10001, display: 'flex', flexDirection: 'column', gap: '8px', diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index 0193f40d4..decae09a2 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -32,13 +32,18 @@ function AppearanceEffects() { const color = settings.mentionHighlightColor; if (color) { document.body.style.setProperty('--mention-highlight-bg', color); - // compute black or white text based on hex luminance + // WCAG 2.1 relative luminance with gamma linearization + const toLinear = (c: number) => { + const s = c / 255; + return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; + }; const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); - const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - document.body.style.setProperty('--mention-highlight-text', lum > 0.5 ? '#000' : '#fff'); - document.body.style.setProperty('--mention-highlight-border', color); + const lum = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b); + document.body.style.setProperty('--mention-highlight-text', lum > 0.179 ? '#000' : '#fff'); + // Derive a visible border: same hue, reduced alpha + document.body.style.setProperty('--mention-highlight-border', `rgba(${r},${g},${b},0.5)`); } else { document.body.style.removeProperty('--mention-highlight-bg'); document.body.style.removeProperty('--mention-highlight-text');