fix(ui): receipt pill, insights counts, settings dividers, decoration preview (#269-#273)
CI / Build & Quality Checks (pull_request) Successful in 2m0s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 6s
CI / Playwright smoke (e2e) (pull_request) Successful in 12m45s
CI / Build & Quality Checks (pull_request) Successful in 2m0s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 6s
CI / Playwright smoke (e2e) (pull_request) Successful in 12m45s
#269 read-receipt pill sits at the message row's right edge, not where the text ends (outside the shrink-to-fit content column; grows in Compact). Bubble layout unchanged. #270 Most Active Members: the bar's % is of its own track, so every row shows its count. #271 Noise Suppression description at T200 like the rest. #272 decoration preview shows your own avatar under the decoration; the empty state is a visible dashed circle. #273 grouped settings get a divider between tiles (SettingTileGroup, via globalStyle; SettingTile roots carry data-setting-tile), and on phones the Sync tile's buttons sit under its description. Fixes #269 Fixes #270 Fixes #271 Fixes #272 Fixes #273 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f5a184e428
commit
53ac87befe
@@ -34,7 +34,8 @@ export function SettingTile({ title, description, before, after, children }: Set
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box alignItems="Center" gap="300">
|
// data-setting-tile: lets a group card draw dividers between tiles only (#273).
|
||||||
|
<Box alignItems="Center" gap="300" data-setting-tile="">
|
||||||
{before && <Box shrink="No">{before}</Box>}
|
{before && <Box shrink="No">{before}</Box>}
|
||||||
<Box grow="Yes" direction="Column" gap="100">
|
<Box grow="Yes" direction="Column" gap="100">
|
||||||
{title && (
|
{title && (
|
||||||
|
|||||||
@@ -401,6 +401,9 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) {
|
|||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<Box alignItems="Center" gap="200">
|
<Box alignItems="Center" gap="200">
|
||||||
|
{/* [Gitea #270] The bar's % width is of its own track,
|
||||||
|
not of the row, so the count always fits beside it. */}
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: 6,
|
height: 6,
|
||||||
@@ -408,9 +411,9 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) {
|
|||||||
background: color.Primary.Main,
|
background: color.Primary.Main,
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
transition: 'width 0.3s ease',
|
transition: 'width 0.3s ease',
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<Text size="T200" priority="300" style={{ flexShrink: 0 }}>
|
<Text size="T200" priority="300" style={{ flexShrink: 0 }}>
|
||||||
{count}
|
{count}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1137,7 +1137,12 @@ export const Message = React.memo(
|
|||||||
</AvatarBase>
|
</AvatarBase>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// [Gitea #269] The content column shrinks to the message's width; the
|
||||||
|
// receipt pill / delivery status sit OUTSIDE it, in a full-width column,
|
||||||
|
// so their `margin-left: auto` reaches the row's right edge instead of
|
||||||
|
// wherever the text happens to end.
|
||||||
const msgContentJSX = (
|
const msgContentJSX = (
|
||||||
|
<Box direction="Column" style={{ maxWidth: '100%', flexGrow: 1, minWidth: 0 }}>
|
||||||
<Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}>
|
<Box direction="Column" alignSelf="Start" style={{ maxWidth: '100%' }}>
|
||||||
{reply}
|
{reply}
|
||||||
{edit && onEditId ? (
|
{edit && onEditId ? (
|
||||||
@@ -1156,6 +1161,7 @@ export const Message = React.memo(
|
|||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
{reactions}
|
{reactions}
|
||||||
|
</Box>
|
||||||
{readReceiptUsers.length > 0 && (
|
{readReceiptUsers.length > 0 && (
|
||||||
<ReadReceiptAvatars
|
<ReadReceiptAvatars
|
||||||
room={room}
|
room={room}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Box, Button, Chip, Icon, Icons, Input, Text, Spinner, color } from 'folds';
|
import { Avatar, Box, Button, Chip, Icon, Icons, Input, Text, Spinner, color } from 'folds';
|
||||||
import { Method } from 'matrix-js-sdk';
|
import { Method } from 'matrix-js-sdk';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||||
|
import { useUserProfile } from '../../../hooks/useUserProfile';
|
||||||
|
import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
|
import { nameInitials } from '../../../utils/common';
|
||||||
|
import { UserAvatar } from '../../../components/user-avatar';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +19,8 @@ import { invalidateDecorationCache } from '../../../hooks/useAvatarDecoration';
|
|||||||
|
|
||||||
const PROFILE_FIELD = 'io.lotus.avatar_decoration';
|
const PROFILE_FIELD = 'io.lotus.avatar_decoration';
|
||||||
const CELL_SIZE = 72;
|
const CELL_SIZE = 72;
|
||||||
|
// Matches AvatarDecoration's default inset, so the preview looks like the timeline.
|
||||||
|
const PREVIEW_INSET = 8;
|
||||||
|
|
||||||
function DecorationPreviewCell({
|
function DecorationPreviewCell({
|
||||||
slug,
|
slug,
|
||||||
@@ -86,6 +93,12 @@ function categoryOf(slug: string | null): string {
|
|||||||
export function ProfileDecoration() {
|
export function ProfileDecoration() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const userId = mx.getUserId()!;
|
const userId = mx.getUserId()!;
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
|
const profile = useUserProfile(userId);
|
||||||
|
const myName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId;
|
||||||
|
const myAvatarUrl = profile.avatarUrl
|
||||||
|
? (mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const [current, setCurrent] = useState<string | null>(null);
|
const [current, setCurrent] = useState<string | null>(null);
|
||||||
const [selected, setSelected] = useState<string | null>(null);
|
const [selected, setSelected] = useState<string | null>(null);
|
||||||
@@ -198,6 +211,10 @@ export function ProfileDecoration() {
|
|||||||
<Box direction="Column" gap="300">
|
<Box direction="Column" gap="300">
|
||||||
{/* Current selection preview */}
|
{/* Current selection preview */}
|
||||||
<Box alignItems="Center" gap="300">
|
<Box alignItems="Center" gap="300">
|
||||||
|
{/* [Gitea #272] Your own avatar with the decoration overlaid, as the
|
||||||
|
timeline draws it (AvatarDecoration: the image is the avatar plus
|
||||||
|
an 8px margin on every side). With no decoration the circle gets
|
||||||
|
a visible dashed outline instead of vanishing into the card. */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -205,10 +222,29 @@ export function ProfileDecoration() {
|
|||||||
height: CELL_SIZE,
|
height: CELL_SIZE,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: color.SurfaceVariant.Container,
|
boxSizing: 'border-box',
|
||||||
overflow: 'hidden',
|
border: selected
|
||||||
|
? 'none'
|
||||||
|
: `2px dashed color-mix(in srgb, ${color.SurfaceVariant.OnContainer} 45%, transparent)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Avatar
|
||||||
|
size="500"
|
||||||
|
radii="Pill"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: PREVIEW_INSET,
|
||||||
|
width: CELL_SIZE - PREVIEW_INSET * 2,
|
||||||
|
height: CELL_SIZE - PREVIEW_INSET * 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
userId={userId}
|
||||||
|
src={myAvatarUrl}
|
||||||
|
alt=""
|
||||||
|
renderFallback={() => <Text size="H5">{nameInitials(myName)}</Text>}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
{selected && (
|
{selected && (
|
||||||
<img
|
<img
|
||||||
src={decorationUrl(selected)}
|
src={decorationUrl(selected)}
|
||||||
@@ -220,6 +256,7 @@ export function ProfileDecoration() {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ import {
|
|||||||
translationUnavailableText,
|
translationUnavailableText,
|
||||||
} from '../../../utils/translation/langUtils';
|
} from '../../../utils/translation/langUtils';
|
||||||
import { chromeTranslationEngine } from '../../../utils/translation/chromeEngine';
|
import { chromeTranslationEngine } from '../../../utils/translation/chromeEngine';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SequenceCardStyle, SettingTileGroup } from '../styles.css';
|
||||||
|
import { ScreenSize, useScreenSize } from '../../../hooks/useScreenSize';
|
||||||
import { UpdateProgress, useTauriUpdater } from '../../../hooks/useTauriUpdater';
|
import { UpdateProgress, useTauriUpdater } from '../../../hooks/useTauriUpdater';
|
||||||
import {
|
import {
|
||||||
describeUpdateError,
|
describeUpdateError,
|
||||||
@@ -1598,6 +1599,7 @@ function Privacy() {
|
|||||||
// [Gitea #104] Sync toggle + the two manual actions. The toggle is itself
|
// [Gitea #104] Sync toggle + the two manual actions. The toggle is itself
|
||||||
// device-local (never synced) so switching it off here sticks.
|
// device-local (never synced) so switching it off here sticks.
|
||||||
function SettingsSyncSection() {
|
function SettingsSyncSection() {
|
||||||
|
const isMobile = useScreenSize() === ScreenSize.Mobile;
|
||||||
const [settingsSync, setSettingsSync] = useSetting(settingsAtom, 'settingsSync');
|
const [settingsSync, setSettingsSync] = useSetting(settingsAtom, 'settingsSync');
|
||||||
const { pushNow, clearRemote } = useSettingsSyncActions();
|
const { pushNow, clearRemote } = useSettingsSyncActions();
|
||||||
const [busy, setBusy] = useState<'push' | 'clear' | null>(null);
|
const [busy, setBusy] = useState<'push' | 'clear' | null>(null);
|
||||||
@@ -1621,22 +1623,7 @@ function SettingsSyncSection() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const syncButtons = (
|
||||||
<Box direction="Column" gap="100">
|
|
||||||
<Text size="L400">Sync</Text>
|
|
||||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
|
||||||
<SettingTile
|
|
||||||
title="Sync Settings Across Devices"
|
|
||||||
description="Keep your preferences (theme, composer toolbar, notifications, quiet hours, call keys…) the same on every device, stored as account data on your own homeserver. Device-specific settings such as zoom, media auto-load, noise suppression and volumes stay local."
|
|
||||||
after={<Switch variant="Primary" value={settingsSync} onChange={setSettingsSync} />}
|
|
||||||
/>
|
|
||||||
</SequenceCard>
|
|
||||||
{settingsSync && (
|
|
||||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
|
||||||
<SettingTile
|
|
||||||
title="Use This Device's Settings Everywhere"
|
|
||||||
description="Push this device's current preferences so every other device adopts them, even ones that changed something more recently."
|
|
||||||
after={
|
|
||||||
<Box gap="200">
|
<Box gap="200">
|
||||||
<Button
|
<Button
|
||||||
size="300"
|
size="300"
|
||||||
@@ -1662,8 +1649,29 @@ function SettingsSyncSection() {
|
|||||||
<Text size="B300">Clear synced copy</Text>
|
<Text size="B300">Clear synced copy</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Text size="L400">Sync</Text>
|
||||||
|
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||||
|
<SettingTile
|
||||||
|
title="Sync Settings Across Devices"
|
||||||
|
description="Keep your preferences (theme, composer toolbar, notifications, quiet hours, call keys…) the same on every device, stored as account data on your own homeserver. Device-specific settings such as zoom, media auto-load, noise suppression and volumes stay local."
|
||||||
|
after={<Switch variant="Primary" value={settingsSync} onChange={setSettingsSync} />}
|
||||||
/>
|
/>
|
||||||
|
</SequenceCard>
|
||||||
|
{settingsSync && (
|
||||||
|
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||||
|
<SettingTile
|
||||||
|
title="Use This Device's Settings Everywhere"
|
||||||
|
description="Push this device's current preferences so every other device adopts them, even ones that changed something more recently."
|
||||||
|
// [Gitea #273] On phones the two buttons squeezed the text into a
|
||||||
|
// narrow column; put them under the description there.
|
||||||
|
after={isMobile ? undefined : syncButtons}
|
||||||
|
>
|
||||||
|
{isMobile && <Box style={{ paddingTop: config.space.S100 }}>{syncButtons}</Box>}
|
||||||
|
</SettingTile>
|
||||||
{note && (
|
{note && (
|
||||||
<Text
|
<Text
|
||||||
size="T200"
|
size="T200"
|
||||||
@@ -1800,7 +1808,8 @@ function Calls() {
|
|||||||
title="Noise Suppression"
|
title="Noise Suppression"
|
||||||
description={
|
description={
|
||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
<Text>
|
{/* [Gitea #271] T200 like every other setting description. */}
|
||||||
|
<Text size="T200">
|
||||||
Filter background noise from your mic during calls. Browser-native uses the built-in
|
Filter background noise from your mic during calls. Browser-native uses the built-in
|
||||||
WebRTC suppressor (Google NSNet2). ML runs a dedicated model for stronger removal.
|
WebRTC suppressor (Google NSNet2). ML runs a dedicated model for stronger removal.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -2033,7 +2042,7 @@ function Calls() {
|
|||||||
)}
|
)}
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
className={SequenceCardStyle}
|
className={`${SequenceCardStyle} ${SettingTileGroup}`}
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
@@ -2130,7 +2139,7 @@ function Calls() {
|
|||||||
)}
|
)}
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
className={SequenceCardStyle}
|
className={`${SequenceCardStyle} ${SettingTileGroup}`}
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="400"
|
gap="400"
|
||||||
@@ -2264,7 +2273,11 @@ function Calls() {
|
|||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
|
|
||||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
<SequenceCard
|
||||||
|
className={`${SequenceCardStyle} ${SettingTileGroup}`}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
direction="Column"
|
||||||
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Soundboard"
|
title="Soundboard"
|
||||||
description="Show a soundboard button in the call bar. Upload short audio clips (like custom emojis) to play them into the call. Clips sync across your devices."
|
description="Show a soundboard button in the call bar. Upload short audio clips (like custom emojis) to play them into the call. Clips sync across your devices."
|
||||||
@@ -2751,7 +2764,11 @@ function Messages() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
<SequenceCard
|
||||||
|
className={`${SequenceCardStyle} ${SettingTileGroup}`}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
direction="Column"
|
||||||
|
>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Translate Messages Into"
|
title="Translate Messages Into"
|
||||||
description={
|
description={
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
import { config } from 'folds';
|
import { color, config } from 'folds';
|
||||||
|
|
||||||
export const SequenceCardStyle = style({
|
export const SequenceCardStyle = style({
|
||||||
padding: config.space.S300,
|
padding: config.space.S300,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Gitea #273] Several settings share one card; without a separator the next
|
||||||
|
* tile's title read like part of the previous description. A thin line goes
|
||||||
|
* between tiles only (not between a tile and its own slider or note).
|
||||||
|
*/
|
||||||
|
export const SettingTileGroup = style({});
|
||||||
|
// globalStyle: vanilla-extract's `selectors` may only target the element itself.
|
||||||
|
globalStyle(`${SettingTileGroup} > [data-setting-tile] ~ [data-setting-tile]`, {
|
||||||
|
borderTop: `1px solid color-mix(in srgb, ${color.SurfaceVariant.OnContainer} 14%, transparent)`,
|
||||||
|
paddingTop: config.space.S400,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user