chore: prettier format all files, brotli, Sentry release tagging, CI gates

Prettier: auto-formatted 103 files to fix baseline. Prettier check in CI
  is now a hard gate (removed continue-on-error).

Brotli: installed libnginx-mod-http-brotli-filter/static. Enabled in nginx
  with brotli_static on for pre-compressed assets and comp_level 6.

Sentry releases: deploy script now exports VITE_APP_VERSION=<git-short-sha>
  before building so each Sentry release maps to an exact commit.
  CI also passes github.sha as VITE_APP_VERSION.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lotus Bot
2026-05-21 20:49:33 -04:00
parent 408fc1b846
commit 42b9cc2b64
105 changed files with 2749 additions and 1850 deletions
+238 -57
View File
@@ -137,7 +137,10 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
useEffect(() => {
const remaining = info.senderTs + info.lifetime - Date.now();
if (remaining <= 0) { onIgnore(); return; }
if (remaining <= 0) {
onIgnore();
return;
}
const id = setTimeout(onIgnore, remaining);
return () => clearTimeout(id);
}, [info.senderTs, info.lifetime, onIgnore]);
@@ -157,7 +160,9 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
<Dialog style={{ maxWidth: toRem(324) }}>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="700">
<Text size="T200" align="Center">
{getMemberDisplayName(info.room, info.sender) ?? getMxIdLocalPart(info.sender) ?? info.sender}
{getMemberDisplayName(info.room, info.sender) ??
getMxIdLocalPart(info.sender) ??
info.sender}
</Text>
<Box direction="Column" gap="500" alignItems="Center">
<Box shrink="No">
@@ -294,7 +299,8 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
const refEventId = relation?.event_id;
const mention =
content['m.mentions']?.room || content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
content['m.mentions']?.room ||
content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) {
return;
}
@@ -410,10 +416,19 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
const { navigateRoom } = useRoomNavigate();
const pipDragRef = React.useRef<{
startX: number; startY: number; origLeft: number; origTop: number; dragged: boolean;
startX: number;
startY: number;
origLeft: number;
origTop: number;
dragged: boolean;
} | null>(null);
const activeDragCleanupRef = React.useRef<(() => void) | null>(null);
React.useEffect(() => () => { activeDragCleanupRef.current?.(); }, []);
React.useEffect(
() => () => {
activeDragCleanupRef.current?.();
},
[]
);
// Track previous pipMode to only reset position when first entering pip (not on callVisible changes)
const prevPipModeRef = React.useRef(false);
@@ -422,16 +437,35 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
if (!el) return;
if (pipMode) {
if (!prevPipModeRef.current) {
el.style.top = 'auto'; el.style.left = 'auto';
el.style.bottom = '72px'; el.style.right = '16px';
el.style.width = '280px'; el.style.height = '158px';
el.style.borderRadius = '12px'; el.style.overflow = 'hidden';
el.style.zIndex = '99'; el.style.boxShadow = '0 8px 32px rgba(0,0,0,0.55)';
el.style.top = 'auto';
el.style.left = 'auto';
el.style.bottom = '72px';
el.style.right = '16px';
el.style.width = '280px';
el.style.height = '158px';
el.style.borderRadius = '12px';
el.style.overflow = 'hidden';
el.style.zIndex = '99';
el.style.boxShadow = '0 8px 32px rgba(0,0,0,0.55)';
el.style.border = '1px solid rgba(255,255,255,0.1)';
}
el.style.visibility = 'visible';
} else {
['top','left','bottom','right','width','height','borderRadius','overflow','zIndex','boxShadow','border'].forEach(p => { (el.style as any)[p] = ''; });
[
'top',
'left',
'bottom',
'right',
'width',
'height',
'borderRadius',
'overflow',
'zIndex',
'boxShadow',
'border',
].forEach((p) => {
(el.style as any)[p] = '';
});
el.style.visibility = callVisible ? '' : 'hidden';
}
prevPipModeRef.current = pipMode;
@@ -444,30 +478,66 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
if (!el) return;
const l = parseFloat(el.style.left);
const t = parseFloat(el.style.top);
if (!isNaN(l)) el.style.left = `${Math.max(0, Math.min(l, window.innerWidth - el.offsetWidth))}px`;
if (!isNaN(t)) el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`;
if (!isNaN(l))
el.style.left = `${Math.max(0, Math.min(l, window.innerWidth - el.offsetWidth))}px`;
if (!isNaN(t))
el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`;
};
window.addEventListener('resize', onPipWindowResize);
return () => window.removeEventListener('resize', onPipWindowResize);
}, [pipMode, callEmbedRef]);
const handlePipMouseDown = (e: React.MouseEvent) => {
const el = callEmbedRef.current; if (!el) return;
const el = callEmbedRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
pipDragRef.current = { startX: e.clientX, startY: e.clientY, origLeft: rect.left, origTop: rect.top, dragged: false };
pipDragRef.current = {
startX: e.clientX,
startY: e.clientY,
origLeft: rect.left,
origTop: rect.top,
dragged: false,
};
const onMove = (ev: MouseEvent) => {
if (!pipDragRef.current || !el) return;
const dx = ev.clientX - pipDragRef.current.startX, dy = ev.clientY - pipDragRef.current.startY;
if (!pipDragRef.current.dragged && Math.abs(dx)+Math.abs(dy) > 5) { pipDragRef.current.dragged = true; document.body.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; }
const dx = ev.clientX - pipDragRef.current.startX,
dy = ev.clientY - pipDragRef.current.startY;
if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5) {
pipDragRef.current.dragged = true;
document.body.style.cursor = 'grabbing';
document.body.style.userSelect = 'none';
}
if (pipDragRef.current.dragged) {
el.style.left = `${Math.max(0, Math.min(window.innerWidth-el.offsetWidth, pipDragRef.current.origLeft+dx))}px`;
el.style.top = `${Math.max(0, Math.min(window.innerHeight-el.offsetHeight, pipDragRef.current.origTop+dy))}px`;
el.style.right = 'auto'; el.style.bottom = 'auto';
el.style.left = `${Math.max(
0,
Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx)
)}px`;
el.style.top = `${Math.max(
0,
Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy)
)}px`;
el.style.right = 'auto';
el.style.bottom = 'auto';
}
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; activeDragCleanupRef.current = null; setTimeout(() => { if (pipDragRef.current) pipDragRef.current.dragged = false; }, 0); };
activeDragCleanupRef.current = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; document.body.style.userSelect = ''; };
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
activeDragCleanupRef.current = null;
setTimeout(() => {
if (pipDragRef.current) pipDragRef.current.dragged = false;
}, 0);
};
activeDragCleanupRef.current = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
};
const handlePipTouchStart = (e: React.TouchEvent) => {
@@ -475,50 +545,115 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
if (!el || e.touches.length !== 1) return;
const touch = e.touches[0];
const rect = el.getBoundingClientRect();
pipDragRef.current = { startX: touch.clientX, startY: touch.clientY, origLeft: rect.left, origTop: rect.top, dragged: false };
pipDragRef.current = {
startX: touch.clientX,
startY: touch.clientY,
origLeft: rect.left,
origTop: rect.top,
dragged: false,
};
const onTouchMove = (ev: TouchEvent) => {
if (!pipDragRef.current || !el || ev.touches.length !== 1) return;
ev.preventDefault();
const t = ev.touches[0];
const dx = t.clientX - pipDragRef.current.startX;
const dy = t.clientY - pipDragRef.current.startY;
if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5) pipDragRef.current.dragged = true;
if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5)
pipDragRef.current.dragged = true;
if (pipDragRef.current.dragged) {
el.style.left = `${Math.max(0, Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx))}px`;
el.style.top = `${Math.max(0, Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy))}px`;
el.style.right = 'auto'; el.style.bottom = 'auto';
el.style.left = `${Math.max(
0,
Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx)
)}px`;
el.style.top = `${Math.max(
0,
Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy)
)}px`;
el.style.right = 'auto';
el.style.bottom = 'auto';
}
};
const onTouchEnd = () => {
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
activeDragCleanupRef.current = null;
setTimeout(() => { if (pipDragRef.current) pipDragRef.current.dragged = false; }, 0);
setTimeout(() => {
if (pipDragRef.current) pipDragRef.current.dragged = false;
}, 0);
};
activeDragCleanupRef.current = () => {
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
};
activeDragCleanupRef.current = () => { document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); };
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('touchend', onTouchEnd);
};
const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
e.stopPropagation(); e.preventDefault();
const el = callEmbedRef.current; if (!el) return;
e.stopPropagation();
e.preventDefault();
const el = callEmbedRef.current;
if (!el) return;
normaliseToTopLeft(el);
const sx = e.clientX, sy = e.clientY, sw = el.offsetWidth, sh = el.offsetHeight;
const sl = parseFloat(el.style.left), st = parseFloat(el.style.top);
document.body.style.cursor = `${corner}-resize`; document.body.style.userSelect = 'none';
const sx = e.clientX,
sy = e.clientY,
sw = el.offsetWidth,
sh = el.offsetHeight;
const sl = parseFloat(el.style.left),
st = parseFloat(el.style.top);
document.body.style.cursor = `${corner}-resize`;
document.body.style.userSelect = 'none';
const onMove = (ev: MouseEvent) => {
const dx = ev.clientX-sx, dy = ev.clientY-sy;
let w = sw, h = sh, l = sl, t = st;
if (corner==='se'){w=sw+dx;h=sh+dy;} if (corner==='sw'){w=sw-dx;h=sh+dy;l=sl+sw-Math.max(PIP_MIN_W,w);}
if (corner==='ne'){w=sw+dx;h=sh-dy;t=st+sh-Math.max(PIP_MIN_H,h);} if (corner==='nw'){w=sw-dx;h=sh-dy;l=sl+sw-Math.max(PIP_MIN_W,w);t=st+sh-Math.max(PIP_MIN_H,h);}
w=Math.max(PIP_MIN_W,Math.min(w,window.innerWidth)); h=Math.max(PIP_MIN_H,Math.min(h,window.innerHeight));
l=Math.max(0,Math.min(l,window.innerWidth-PIP_MIN_W)); t=Math.max(0,Math.min(t,window.innerHeight-PIP_MIN_H));
el.style.width=`${w}px`; el.style.height=`${h}px`; el.style.left=`${l}px`; el.style.top=`${t}px`;
const dx = ev.clientX - sx,
dy = ev.clientY - sy;
let w = sw,
h = sh,
l = sl,
t = st;
if (corner === 'se') {
w = sw + dx;
h = sh + dy;
}
if (corner === 'sw') {
w = sw - dx;
h = sh + dy;
l = sl + sw - Math.max(PIP_MIN_W, w);
}
if (corner === 'ne') {
w = sw + dx;
h = sh - dy;
t = st + sh - Math.max(PIP_MIN_H, h);
}
if (corner === 'nw') {
w = sw - dx;
h = sh - dy;
l = sl + sw - Math.max(PIP_MIN_W, w);
t = st + sh - Math.max(PIP_MIN_H, h);
}
w = Math.max(PIP_MIN_W, Math.min(w, window.innerWidth));
h = Math.max(PIP_MIN_H, Math.min(h, window.innerHeight));
l = Math.max(0, Math.min(l, window.innerWidth - PIP_MIN_W));
t = Math.max(0, Math.min(t, window.innerHeight - PIP_MIN_H));
el.style.width = `${w}px`;
el.style.height = `${h}px`;
el.style.left = `${l}px`;
el.style.top = `${t}px`;
};
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor=''; document.body.style.userSelect=''; activeDragCleanupRef.current = null; };
activeDragCleanupRef.current = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor=''; document.body.style.userSelect=''; };
document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp);
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
activeDragCleanupRef.current = null;
};
activeDragCleanupRef.current = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
};
return (
@@ -548,25 +683,71 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
aria-label="Return to call"
onMouseDown={handlePipMouseDown}
onTouchStart={handlePipTouchStart}
onClick={() => { if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId); }}
onClick={() => {
if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId);
}}
onKeyDown={(e) => e.key === 'Enter' && navigateRoom(callEmbed.roomId)}
style={{ position:'absolute', inset:0, zIndex:1, background:'transparent', cursor:'grab', display:'flex', alignItems:'flex-start', justifyContent:'flex-end', padding:'6px' }}
style={{
position: 'absolute',
inset: 0,
zIndex: 1,
background: 'transparent',
cursor: 'grab',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-end',
padding: '6px',
}}
>
<div style={{ background:'rgba(0,0,0,0.65)', backdropFilter:'blur(4px)', borderRadius:'6px', padding:'3px 8px', color:'#fff', fontSize:'11px', fontWeight:600, pointerEvents:'none' }}>
<div
style={{
background: 'rgba(0,0,0,0.65)',
backdropFilter: 'blur(4px)',
borderRadius: '6px',
padding: '3px 8px',
color: '#fff',
fontSize: '11px',
fontWeight: 600,
pointerEvents: 'none',
}}
>
Return to call
</div>
</div>
{(['se','sw','ne','nw'] as Corner[]).map((corner) => {
const s = corner.includes('s'); const e2 = corner.includes('e');
const dots = [[2,2],[2,7],[7,2]].map(([a,b]) => ({
position:'absolute' as const, width:4, height:4, borderRadius:'50%',
background:'rgba(255,255,255,0.45)',
[s?'bottom':'top']:a, [e2?'right':'left']:b,
{(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => {
const s = corner.includes('s');
const e2 = corner.includes('e');
const dots = [
[2, 2],
[2, 7],
[7, 2],
].map(([a, b]) => ({
position: 'absolute' as const,
width: 4,
height: 4,
borderRadius: '50%',
background: 'rgba(255,255,255,0.45)',
[s ? 'bottom' : 'top']: a,
[e2 ? 'right' : 'left']: b,
}));
return (
<div key={corner} onMouseDown={(ev) => handleResizeMouseDown(ev, corner)} onClick={(ev) => ev.stopPropagation()}
style={{ position:'absolute', width:'18px', height:'18px', [s?'bottom':'top']:0, [e2?'right':'left']:0, cursor:`${corner}-resize`, zIndex:2 }}>
{dots.map((style, i) => <div key={i} style={style} />)}
<div
key={corner}
onMouseDown={(ev) => handleResizeMouseDown(ev, corner)}
onClick={(ev) => ev.stopPropagation()}
style={{
position: 'absolute',
width: '18px',
height: '18px',
[s ? 'bottom' : 'top']: 0,
[e2 ? 'right' : 'left']: 0,
cursor: `${corner}-resize`,
zIndex: 2,
}}
>
{dots.map((style, i) => (
<div key={i} style={style} />
))}
</div>
);
})}
+9 -2
View File
@@ -259,9 +259,16 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
<Dialog variant="Surface">
<Header style={DialogHeaderStyles} variant="Surface" size="500">
<Box grow="Yes">
<Text as="h2" size="H4">Device Verification</Text>
<Text as="h2" size="H4">
Device Verification
</Text>
</Box>
<IconButton size="300" radii="300" onClick={handleCancel} aria-label="Cancel verification">
<IconButton
size="300"
radii="300"
onClick={handleCancel}
aria-label="Cancel verification"
>
<Icon src={Icons.Cross} />
</IconButton>
</Header>
@@ -299,7 +299,9 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">Setup Device Verification</Text>
<Text as="h2" size="H4">
Setup Device Verification
</Text>
</Box>
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
<Icon src={Icons.Cross} />
@@ -334,7 +336,9 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">Reset Device Verification</Text>
<Text as="h2" size="H4">
Reset Device Verification
</Text>
</Box>
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
<Icon src={Icons.Cross} />
+20 -13
View File
@@ -8,7 +8,6 @@ import { settingsAtom } from '../state/settings';
const PICKER_WIDTH = 312;
type GifPickerInnerProps = {
onSelect: (url: string, width: number, height: number) => void;
requestClose: () => void;
@@ -34,23 +33,27 @@ function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInne
return (
<Box direction="Column" style={{ width: `${PICKER_WIDTH}px` }}>
{lotusTerminal && (
<div style={{
padding: '5px 10px 4px',
borderBottom: '1px solid rgba(255,107,0,0.2)',
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
fontSize: '10px',
fontWeight: 700,
letterSpacing: '0.1em',
color: '#FF6B00',
userSelect: 'none',
}}>
<div
style={{
padding: '5px 10px 4px',
borderBottom: '1px solid rgba(255,107,0,0.2)',
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
fontSize: '10px',
fontWeight: 700,
letterSpacing: '0.1em',
color: '#FF6B00',
userSelect: 'none',
}}
>
// GIF_SEARCH
</div>
)}
<Box style={{ padding: '8px 8px 4px' }}>
<SearchBar style={{ width: '100%', borderRadius: lotusTerminal ? '4px' : '8px' }} />
</Box>
<div style={{ overflowY: 'auto', overflowX: 'hidden', maxHeight: '340px', padding: '0 8px 8px' }}>
<div
style={{ overflowY: 'auto', overflowX: 'hidden', maxHeight: '340px', padding: '0 8px 8px' }}
>
<Grid
key={searchKey}
fetchGifs={fetchGifs}
@@ -108,7 +111,11 @@ export function GifPicker({ apiKey, onSelect, requestClose }: GifPickerProps) {
style={containerStyle}
>
<SearchContextManager apiKey={apiKey} initialTerm="">
<GifPickerInner onSelect={onSelect} requestClose={requestClose} lotusTerminal={!!lotusTerminal} />
<GifPickerInner
onSelect={onSelect}
requestClose={requestClose}
lotusTerminal={!!lotusTerminal}
/>
</SearchContextManager>
</Box>
</FocusTrap>
+3 -1
View File
@@ -43,7 +43,9 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">Logout</Text>
<Text as="h2" size="H4">
Logout
</Text>
</Box>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+33 -13
View File
@@ -22,7 +22,8 @@ export function RoomSkeleton() {
`;
const shimmer = {
background: 'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
background:
'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
backgroundSize: '800px 100%',
animation: `shimmer-${id} 1.6s ease-in-out infinite`,
borderRadius: '4px',
@@ -32,16 +33,18 @@ export function RoomSkeleton() {
<>
<style>{shimmerKeyframes}</style>
<div
style={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
height: '100%',
overflow: 'hidden',
// CSS vars resolve against both light and dark themes automatically
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
} as React.CSSProperties}
style={
{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
height: '100%',
overflow: 'hidden',
// CSS vars resolve against both light and dark themes automatically
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
} as React.CSSProperties
}
>
{/* Header — matches PageHeader size="600" (56px) */}
<div
@@ -56,7 +59,15 @@ export function RoomSkeleton() {
}}
>
{/* Avatar */}
<div style={{ ...shimmer, width: '32px', height: '32px', borderRadius: '50%', flexShrink: 0 }} />
<div
style={{
...shimmer,
width: '32px',
height: '32px',
borderRadius: '50%',
flexShrink: 0,
}}
/>
{/* Room name */}
<div style={{ ...shimmer, width: '140px', height: '16px' }} />
{/* Spacer */}
@@ -70,7 +81,16 @@ export function RoomSkeleton() {
<div style={{ flex: 1, overflowY: 'hidden', padding: '16px 0' }}>
{MESSAGES.map((msg, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={i} style={{ display: 'flex', gap: '12px', padding: '4px 16px', alignItems: 'flex-start', marginBottom: msg.showAvatar ? '8px' : '2px' }}>
<div
key={i}
style={{
display: 'flex',
gap: '12px',
padding: '4px 16px',
alignItems: 'flex-start',
marginBottom: msg.showAvatar ? '8px' : '2px',
}}
>
{/* Avatar — only shown on first message in a group */}
<div style={{ width: '36px', flexShrink: 0 }}>
{msg.showAvatar && (
+11 -2
View File
@@ -23,7 +23,11 @@ export function EditorPreview() {
return (
<>
<IconButton variant="SurfaceVariant" aria-label="Open editor preview" onClick={() => setOpen(!open)}>
<IconButton
variant="SurfaceVariant"
aria-label="Open editor preview"
onClick={() => setOpen(!open)}
>
<Icon src={Icons.BlockQuote} />
</IconButton>
<Overlay open={open} backdrop={<OverlayBackdrop />}>
@@ -58,7 +62,12 @@ export function EditorPreview() {
>
<Icon src={toolbar ? Icons.AlphabetUnderline : Icons.Alphabet} />
</IconButton>
<IconButton variant="SurfaceVariant" size="300" radii="300" aria-label="Insert emoji">
<IconButton
variant="SurfaceVariant"
size="300"
radii="300"
aria-label="Insert emoji"
>
<Icon src={Icons.Smile} />
</IconButton>
<IconButton variant="SurfaceVariant" size="300" radii="300" aria-label="Send">
+32 -30
View File
@@ -279,44 +279,42 @@ export function Toolbar() {
<MarkButton
format={MarkType.Bold}
icon={Icons.Bold}
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`}
label="Bold"
/>}
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} label="Bold" />}
/>
<MarkButton
format={MarkType.Italic}
icon={Icons.Italic}
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`}
label="Italic"
/>}
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} label="Italic" />}
/>
<MarkButton
format={MarkType.Underline}
icon={Icons.Underline}
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`}
label="Underline"
/>}
tooltip={
<BtnTooltip text="Underline" shortCode={`${modKey} + U`} label="Underline" />
}
/>
<MarkButton
format={MarkType.StrikeThrough}
icon={Icons.Strike}
tooltip={<BtnTooltip text="Strike Through" shortCode={`${modKey} + S`}
label="Strikethrough"
/>}
tooltip={
<BtnTooltip
text="Strike Through"
shortCode={`${modKey} + S`}
label="Strikethrough"
/>
}
/>
<MarkButton
format={MarkType.Code}
icon={Icons.Code}
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`}
label="Inline code"
/>}
tooltip={
<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} label="Inline code" />
}
/>
<MarkButton
format={MarkType.Spoiler}
icon={Icons.EyeBlind}
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`}
label="Spoiler"
/>}
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} label="Spoiler" />}
/>
</Box>
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
@@ -325,30 +323,34 @@ export function Toolbar() {
<BlockButton
format={BlockType.BlockQuote}
icon={Icons.BlockQuote}
tooltip={<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`}
label="Block quote"
/>}
tooltip={
<BtnTooltip text="Block Quote" shortCode={`${modKey} + '`} label="Block quote" />
}
/>
<BlockButton
format={BlockType.CodeBlock}
icon={Icons.BlockCode}
tooltip={<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`}
label="Code block"
/>}
tooltip={
<BtnTooltip text="Block Code" shortCode={`${modKey} + ;`} label="Code block" />
}
/>
<BlockButton
format={BlockType.OrderedList}
icon={Icons.OrderList}
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`}
label="Ordered list"
/>}
tooltip={
<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} label="Ordered list" />
}
/>
<BlockButton
format={BlockType.UnorderedList}
icon={Icons.UnorderList}
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`}
label="Unordered list"
/>}
tooltip={
<BtnTooltip
text="Unordered List"
shortCode={`${modKey} + 8`}
label="Unordered list"
/>
}
/>
<HeadingBlockButton />
</Box>
@@ -68,10 +68,30 @@ export const EventReaders = as<'div', EventReadersProps>(
className={css.Header}
variant="Surface"
size="600"
style={lotusTerminal ? { borderBottom: '1px solid rgba(0,212,255,0.30)', boxShadow: '0 2px 12px rgba(0,212,255,0.08)' } : undefined}
style={
lotusTerminal
? {
borderBottom: '1px solid rgba(0,212,255,0.30)',
boxShadow: '0 2px 12px rgba(0,212,255,0.08)',
}
: undefined
}
>
<Box grow="Yes">
<Text size="H3" style={lotusTerminal ? { color: '#00D4FF', textShadow: '0 0 6px rgba(0,212,255,0.45)', letterSpacing: '0.05em' } : undefined}>Seen by</Text>
<Text
size="H3"
style={
lotusTerminal
? {
color: '#00D4FF',
textShadow: '0 0 6px rgba(0,212,255,0.45)',
letterSpacing: '0.05em',
}
: undefined
}
>
Seen by
</Text>
</Box>
<IconButton size="300" onClick={requestClose} aria-label="Close">
<Icon src={Icons.Cross} />
@@ -120,9 +140,15 @@ export const EventReaders = as<'div', EventReadersProps>(
{receiptTs !== undefined && (
<Text
size="T200"
style={lotusTerminal
? { color: '#FFB300', textShadow: '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)', fontSize: '0.72rem' }
: { opacity: 0.6 }}
style={
lotusTerminal
? {
color: '#FFB300',
textShadow: '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)',
fontSize: '0.72rem',
}
: { opacity: 0.6 }
}
>
{formatReadTs(receiptTs, hour24Clock)}
</Text>
@@ -80,7 +80,9 @@ export function JoinAddressPrompt({ onOpen, onCancel }: JoinAddressProps) {
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">Join with Address</Text>
<Text as="h2" size="H4">
Join with Address
</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
<Icon src={Icons.Cross} />
@@ -66,7 +66,9 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4" id="leave-room-dialog-title">Leave Room</Text>
<Text as="h2" size="H4" id="leave-room-dialog-title">
Leave Room
</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
<Icon src={Icons.Cross} />
@@ -66,7 +66,9 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">Leave Space</Text>
<Text as="h2" size="H4">
Leave Space
</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
<Icon src={Icons.Cross} />
+7 -1
View File
@@ -48,7 +48,13 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
variant={hasError ? 'Critical' : 'SurfaceVariant'}
size="300"
radii="300"
aria-label={downloading ? 'Downloading...' : hasError ? 'Download failed, click to retry' : 'Download file'}
aria-label={
downloading
? 'Downloading...'
: hasError
? 'Download failed, click to retry'
: 'Download file'
}
>
{downloading ? (
<Spinner size="100" variant={hasError ? 'Critical' : 'Secondary'} />
@@ -394,7 +394,9 @@ export function MLocation({ content }: MLocationProps) {
const lat = parseFloat(location.latitude);
const lon = parseFloat(location.longitude);
if (!isFinite(lat) || !isFinite(lon)) return <BrokenContent />;
const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.007},${lat - 0.004},${lon + 0.007},${lat + 0.004}&layer=mapnik&marker=${lat},${lon}`;
const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.007},${
lat - 0.004
},${lon + 0.007},${lat + 0.004}&layer=mapnik&marker=${lat},${lon}`;
return (
<Box direction="Column" alignItems="Start" gap="200">
+1 -2
View File
@@ -29,8 +29,7 @@ export const Reaction = as<
{reaction.startsWith('mxc://') ? (
<img
className={css.ReactionImg}
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction
}
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction}
alt={reaction}
/>
) : (
@@ -8,7 +8,9 @@ export const MessageDeletedContent = as<'div', { children?: never; reason?: stri
({ reason, ...props }, ref) => (
<Box as="span" alignItems="Center" gap="100" style={warningStyle} {...props} ref={ref}>
<Icon size="50" src={Icons.Delete} />
<i>{reason ? `This message has been deleted — ${reason}` : 'This message has been deleted'}</i>
<i>
{reason ? `This message has been deleted — ${reason}` : 'This message has been deleted'}
</i>
</Box>
)
);
@@ -105,9 +105,9 @@ export function PollContent({
const mx = useMatrixClient();
const isStable = !!content['m.poll'];
const poll = (
content['m.poll'] ?? content['org.matrix.msc3381.poll.start']
) as PollData | undefined;
const poll = (content['m.poll'] ?? content['org.matrix.msc3381.poll.start']) as
| PollData
| undefined;
const [votes, setVotes] = useState<VoteState>(() => {
if (!roomId || !eventId) return { counts: new Map(), myVote: null, total: 0 };
@@ -259,7 +259,9 @@ export function PollContent({
padding: '7px 12px',
borderRadius: '8px',
background: selected ? 'var(--bg-surface-active)' : 'var(--bg-surface-low)',
border: `1px solid ${selected ? 'var(--text-primary)' : 'var(--bg-surface-border)'}`,
border: `1px solid ${
selected ? 'var(--text-primary)' : 'var(--bg-surface-border)'
}`,
fontSize: '0.88rem',
lineHeight: 1.4,
textAlign: 'left',
@@ -281,23 +283,21 @@ export function PollContent({
position: 'absolute',
inset: 0,
width: `${pct}%`,
background: selected
? 'rgba(255,255,255,0.10)'
: 'rgba(255,255,255,0.05)',
background: selected ? 'rgba(255,255,255,0.10)' : 'rgba(255,255,255,0.05)',
borderRadius: '8px',
pointerEvents: 'none',
}}
/>
)}
<span style={{ display: 'flex', alignItems: 'center', gap: '8px', position: 'relative' }}>
<span
style={{ display: 'flex', alignItems: 'center', gap: '8px', position: 'relative' }}
>
<span style={{ flexGrow: 1 }}>{text}</span>
{selected && (
<span style={{ opacity: 0.8, fontSize: '1rem', flexShrink: 0 }}></span>
)}
{total > 0 && (
<span style={{ opacity: 0.6, fontSize: '0.78rem', flexShrink: 0 }}>
{pct}%
</span>
<span style={{ opacity: 0.6, fontSize: '0.78rem', flexShrink: 0 }}>{pct}%</span>
)}
</span>
</button>
@@ -79,7 +79,9 @@ export function ReadReceiptAvatars({
style={{
display: 'flex',
alignItems: 'center',
backgroundColor: lotusTerminal ? 'rgba(0,212,255,0.07)' : color.SurfaceVariant.Container,
backgroundColor: lotusTerminal
? 'rgba(0,212,255,0.07)'
: color.SurfaceVariant.Container,
border: lotusTerminal ? '1px solid rgba(0,212,255,0.30)' : '1px solid transparent',
boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
borderRadius: '999px',
@@ -88,8 +90,7 @@ export function ReadReceiptAvatars({
}}
>
{displayed.map((userId) => {
const name =
getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId;
const avatarMxc = room.getMember(userId)?.getMxcAvatarUrl();
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 32, 32, 'crop') ?? undefined
+3 -1
View File
@@ -18,7 +18,9 @@ function DummyErrorDialog({
<Dialog>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Text as="h2" size="H4">{title}</Text>
<Text as="h2" size="H4">
{title}
</Text>
<Text>{message}</Text>
</Box>
<Button variant="Critical" onClick={onRetry}>
+12 -3
View File
@@ -37,9 +37,16 @@ function EmailErrorDialog({
gap="400"
>
<Box direction="Column" gap="100">
<Text as="h2" size="H4">{title}</Text>
<Text as="h2" size="H4">
{title}
</Text>
<Text>{message}</Text>
<Text as="label" htmlFor="retryEmailInput" size="L400" style={{ paddingTop: config.space.S400 }}>
<Text
as="label"
htmlFor="retryEmailInput"
size="L400"
style={{ paddingTop: config.space.S400 }}
>
Email
</Text>
<Input
@@ -141,7 +148,9 @@ export function EmailStageDialog({
<Dialog>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Text as="h2" size="H4">Verification Request Sent</Text>
<Text as="h2" size="H4">
Verification Request Sent
</Text>
<Text>{`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}</Text>
{errorCode && (
@@ -43,7 +43,9 @@ export function PasswordStage({
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">Account Password</Text>
<Text as="h2" size="H4">
Account Password
</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
<Icon src={Icons.Cross} />
@@ -35,9 +35,16 @@ function RegistrationTokenErrorDialog({
gap="400"
>
<Box direction="Column" gap="100">
<Text as="h2" size="H4">{title}</Text>
<Text as="h2" size="H4">
{title}
</Text>
<Text>{message}</Text>
<Text as="label" htmlFor="retryTokenInput" size="L400" style={{ paddingTop: config.space.S400 }}>
<Text
as="label"
htmlFor="retryTokenInput"
size="L400"
style={{ paddingTop: config.space.S400 }}
>
Registration Token
</Text>
<Input
+3 -1
View File
@@ -54,7 +54,9 @@ export function SSOStage({
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">SSO Login</Text>
<Text as="h2" size="H4">
SSO Login
</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
<Icon src={Icons.Cross} />
+3 -1
View File
@@ -18,7 +18,9 @@ function TermsErrorDialog({
<Dialog>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Text as="h2" size="H4">{title}</Text>
<Text as="h2" size="H4">
{title}
</Text>
<Text>{message}</Text>
</Box>
<Button variant="Critical" onClick={onRetry}>
@@ -4,7 +4,13 @@ import { Box, as } from 'folds';
import * as css from './UrlPreview.css';
export const UrlPreview = as<'div'>(({ className, ...props }, ref) => (
<Box shrink="No" data-url-preview="" className={classNames(css.UrlPreview, className)} {...props} ref={ref} />
<Box
shrink="No"
data-url-preview=""
className={classNames(css.UrlPreview, className)}
{...props}
ref={ref}
/>
));
export const UrlPreviewImg = as<'img'>(({ className, alt, ...props }, ref) => (
@@ -68,7 +68,9 @@ function SelfDemoteAlert({ power, onCancel, onChange }: SelfDemoteAlertProps) {
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">Self Demotion</Text>
<Text as="h2" size="H4">
Self Demotion
</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
<Icon src={Icons.Cross} />
@@ -118,7 +120,9 @@ function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps)
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">Shared Power</Text>
<Text as="h2" size="H4">
Shared Power
</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
<Icon src={Icons.Cross} />