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:
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -198,7 +198,9 @@ export function AddExistingModal({ parentId, space, requestClose }: AddExistingM
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Add Existing</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Add Existing
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton size="300" radii="300" onClick={requestClose} aria-label="Close">
|
||||
|
||||
@@ -62,7 +62,9 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
|
||||
// Track microphone via ref so the PTT effect doesn't need it as a dep (avoids listener churn)
|
||||
const microphoneRef = useRef(microphone);
|
||||
useEffect(() => { microphoneRef.current = microphone; }, [microphone]);
|
||||
useEffect(() => {
|
||||
microphoneRef.current = microphone;
|
||||
}, [microphone]);
|
||||
|
||||
// Handle PTT mode toggle mid-call — save/restore mic state (I-4)
|
||||
const pttModeRef = useRef(pttMode);
|
||||
@@ -165,8 +167,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
setPttActive(false);
|
||||
}
|
||||
};
|
||||
// microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// microphone intentionally read via microphoneRef — excluded from deps to avoid listener churn
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pttMode, pttKey, callEmbed]);
|
||||
|
||||
const [hangupState, hangup] = useAsyncCallback(
|
||||
@@ -184,21 +186,31 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
justifyContent="Center"
|
||||
alignItems="Center"
|
||||
>
|
||||
{pttMode && (
|
||||
lotusTerminal ? (
|
||||
<Box style={{
|
||||
position: 'absolute',
|
||||
top: '-2.5rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: pttActive ? 'rgba(0,255,136,0.18)' : 'rgba(255,107,0,0.12)',
|
||||
border: `1px solid ${pttActive ? 'rgba(0,255,136,0.55)' : 'rgba(255,107,0,0.35)'}`,
|
||||
borderRadius: '99px',
|
||||
padding: '0.2rem 0.9rem',
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<Text size="T200" style={{ color: pttActive ? '#00FF88' : '#FF6B00', fontWeight: 700, letterSpacing: '0.08em', fontFamily: 'JetBrains Mono, monospace' }}>
|
||||
{pttMode &&
|
||||
(lotusTerminal ? (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-2.5rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: pttActive ? 'rgba(0,255,136,0.18)' : 'rgba(255,107,0,0.12)',
|
||||
border: `1px solid ${pttActive ? 'rgba(0,255,136,0.55)' : 'rgba(255,107,0,0.35)'}`,
|
||||
borderRadius: '99px',
|
||||
padding: '0.2rem 0.9rem',
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
size="T200"
|
||||
style={{
|
||||
color: pttActive ? '#00FF88' : '#FF6B00',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.08em',
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
}}
|
||||
>
|
||||
{pttActive ? '● LIVE' : `PTT — Hold ${pttKeyLabel}`}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -221,8 +233,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
{pttActive ? '● Live' : `PTT — Hold ${pttKeyLabel}`}
|
||||
</Text>
|
||||
</Chip>
|
||||
)
|
||||
)}
|
||||
))}
|
||||
{shareConfirm && (
|
||||
<Box
|
||||
style={{
|
||||
@@ -242,17 +253,31 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<Text size="T300" style={{ fontWeight: 600 }}>Share your screen?</Text>
|
||||
<Text size="T200" style={{ opacity: 0.75 }}>Your screen will be visible to all participants in this call.</Text>
|
||||
<Text size="T300" style={{ fontWeight: 600 }}>
|
||||
Share your screen?
|
||||
</Text>
|
||||
<Text size="T200" style={{ opacity: 0.75 }}>
|
||||
Your screen will be visible to all participants in this call.
|
||||
</Text>
|
||||
<Box gap="200">
|
||||
<Button
|
||||
size="300" variant="Success" fill="Solid" radii="300"
|
||||
onClick={() => { callEmbed.control.toggleScreenshare(); setShareConfirm(false); }}
|
||||
size="300"
|
||||
variant="Success"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
onClick={() => {
|
||||
callEmbed.control.toggleScreenshare();
|
||||
setShareConfirm(false);
|
||||
}}
|
||||
>
|
||||
<Text size="B300">Share</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300" variant="Secondary" fill="Soft" radii="300" outlined
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={() => setShareConfirm(false)}
|
||||
>
|
||||
<Text size="B300">Cancel</Text>
|
||||
@@ -281,9 +306,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
||||
<VideoButton enabled={video} onToggle={() => callEmbed.control.toggleVideo()} />
|
||||
<ScreenShareButton
|
||||
enabled={screenshare}
|
||||
onToggle={() => screenshare
|
||||
? callEmbed.control.toggleScreenshare()
|
||||
: setShareConfirm(true)
|
||||
onToggle={() =>
|
||||
screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -108,7 +108,9 @@ export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||
onClick={() => onToggle()}
|
||||
outlined
|
||||
disabled={disabled}
|
||||
aria-label={disabled ? 'Camera disabled in settings' : enabled ? 'Stop camera' : 'Start camera'}
|
||||
aria-label={
|
||||
disabled ? 'Camera disabled in settings' : enabled ? 'Stop camera' : 'Start camera'
|
||||
}
|
||||
aria-pressed={enabled}
|
||||
style={disabled ? { opacity: 0.4, cursor: 'not-allowed' } : undefined}
|
||||
>
|
||||
|
||||
@@ -75,7 +75,10 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
|
||||
</Box>
|
||||
<Box grow="Yes" direction="Column" gap="200">
|
||||
{micDenied && (
|
||||
<Text size="T200" style={{ color: 'var(--tc-critical-high, #e53e3e)', textAlign: 'center' }}>
|
||||
<Text
|
||||
size="T200"
|
||||
style={{ color: 'var(--tc-critical-high, #e53e3e)', textAlign: 'center' }}
|
||||
>
|
||||
Microphone access is blocked. Enable it in your browser settings to join.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -121,9 +121,16 @@ export function RoomEncryption({ permissions }: RoomEncryptionProps) {
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Enable Encryption</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Enable Encryption
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={() => setPrompt(false)} radii="300" aria-label="Cancel">
|
||||
<IconButton
|
||||
size="300"
|
||||
onClick={() => setPrompt(false)}
|
||||
radii="300"
|
||||
aria-label="Cancel"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -103,7 +103,9 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}</Text>
|
||||
<Text as="h2" size="H4">
|
||||
{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={requestClose} radii="300" aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
|
||||
@@ -58,7 +58,9 @@ function CreateSpaceModal({ state }: CreateSpaceModalProps) {
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">New Space</Text>
|
||||
<Text as="h2" size="H4">
|
||||
New Space
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton size="300" radii="300" onClick={closeDialog} aria-label="Close">
|
||||
|
||||
@@ -22,7 +22,9 @@ export function LobbyHero() {
|
||||
const name = useRoomName(space);
|
||||
const topic = useRoomTopic(space);
|
||||
const avatarMxc = useRoomAvatar(space);
|
||||
const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
|
||||
const avatarUrl = avatarMxc
|
||||
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<PageHero
|
||||
|
||||
@@ -147,7 +147,8 @@ const DARK: Record<ChatBackground, CSSProperties> = {
|
||||
// True pointy-top hexagonal grid via SVG data URI
|
||||
hexgrid: {
|
||||
backgroundColor: '#060c14',
|
||||
backgroundImage: 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%280%2C212%2C255%2C0.13%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
|
||||
backgroundImage:
|
||||
'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%280%2C212%2C255%2C0.13%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
|
||||
backgroundSize: '29px 50px',
|
||||
},
|
||||
|
||||
@@ -305,7 +306,8 @@ const LIGHT: Record<ChatBackground, CSSProperties> = {
|
||||
|
||||
hexgrid: {
|
||||
backgroundColor: '#f4f8ff',
|
||||
backgroundImage: 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%2850%2C100%2C220%2C0.11%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
|
||||
backgroundImage:
|
||||
'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%2850%2C100%2C220%2C0.11%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")',
|
||||
backgroundSize: '29px 50px',
|
||||
},
|
||||
|
||||
|
||||
@@ -440,4 +440,4 @@ function RoomNavItem_({
|
||||
</NavItem>
|
||||
);
|
||||
}
|
||||
export const RoomNavItem = React.memo(RoomNavItem_);
|
||||
export const RoomNavItem = React.memo(RoomNavItem_);
|
||||
|
||||
@@ -117,7 +117,11 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<IconButton onClick={requestClose} variant="Background" aria-label="Close settings">
|
||||
<IconButton
|
||||
onClick={requestClose}
|
||||
variant="Background"
|
||||
aria-label="Close settings"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -41,7 +41,12 @@ export function CallChatView() {
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton ref={triggerRef} variant="Surface" onClick={handleClose} aria-label="Close call chat">
|
||||
<IconButton
|
||||
ref={triggerRef}
|
||||
variant="Surface"
|
||||
onClick={handleClose}
|
||||
aria-label="Close call chat"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -30,7 +30,9 @@ import {
|
||||
} from 'folds';
|
||||
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
const GifPicker = React.lazy(() => import('../../components/GifPicker').then((m) => ({ default: m.GifPicker })));
|
||||
const GifPicker = React.lazy(() =>
|
||||
import('../../components/GifPicker').then((m) => ({ default: m.GifPicker }))
|
||||
);
|
||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
||||
import {
|
||||
CustomEditor,
|
||||
@@ -57,7 +59,9 @@ import {
|
||||
getMentions,
|
||||
} from '../../components/editor';
|
||||
import { EmojiBoardTab } from '../../components/emoji-board/types';
|
||||
const EmojiBoard = React.lazy(() => import('../../components/emoji-board').then((m) => ({ default: m.EmojiBoard })));
|
||||
const EmojiBoard = React.lazy(() =>
|
||||
import('../../components/emoji-board').then((m) => ({ default: m.EmojiBoard }))
|
||||
);
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import {
|
||||
TUploadContent,
|
||||
@@ -143,7 +147,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
const powerLevels = usePowerLevelsContext();
|
||||
const creators = useRoomCreators(room);
|
||||
|
||||
const alive = useAlive();
|
||||
const alive = useAlive();
|
||||
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
|
||||
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
|
||||
const replyUserID = replyDraft?.userId;
|
||||
@@ -467,8 +471,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
async (gifUrl: string, w: number, h: number) => {
|
||||
try {
|
||||
// Only fetch from trusted Giphy CDN domains
|
||||
const allowed = ['media.giphy.com', 'i.giphy.com', 'media0.giphy.com',
|
||||
'media1.giphy.com', 'media2.giphy.com', 'media3.giphy.com', 'media4.giphy.com'];
|
||||
const allowed = [
|
||||
'media.giphy.com',
|
||||
'i.giphy.com',
|
||||
'media0.giphy.com',
|
||||
'media1.giphy.com',
|
||||
'media2.giphy.com',
|
||||
'media3.giphy.com',
|
||||
'media4.giphy.com',
|
||||
];
|
||||
const { hostname } = new URL(gifUrl);
|
||||
if (!allowed.includes(hostname)) return;
|
||||
|
||||
@@ -695,24 +706,26 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
||||
}
|
||||
content={
|
||||
<React.Suspense fallback={null}><EmojiBoard
|
||||
tab={emojiBoardTab}
|
||||
onTabChange={setEmojiBoardTab}
|
||||
imagePackRooms={imagePackRooms}
|
||||
returnFocusOnDeactivate={false}
|
||||
onEmojiSelect={handleEmoticonSelect}
|
||||
onCustomEmojiSelect={handleEmoticonSelect}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
requestClose={() => {
|
||||
setEmojiBoardTab((t) => {
|
||||
if (t) {
|
||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||
return undefined;
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}}
|
||||
/></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<EmojiBoard
|
||||
tab={emojiBoardTab}
|
||||
onTabChange={setEmojiBoardTab}
|
||||
imagePackRooms={imagePackRooms}
|
||||
returnFocusOnDeactivate={false}
|
||||
onEmojiSelect={handleEmoticonSelect}
|
||||
onCustomEmojiSelect={handleEmoticonSelect}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
requestClose={() => {
|
||||
setEmojiBoardTab((t) => {
|
||||
if (t) {
|
||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||
return undefined;
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Suspense>
|
||||
}
|
||||
>
|
||||
{!hideStickerBtn && (
|
||||
@@ -765,11 +778,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
: undefined
|
||||
}
|
||||
content={
|
||||
<React.Suspense fallback={null}><GifPicker
|
||||
apiKey={gifApiKey}
|
||||
onSelect={handleGifSelect}
|
||||
requestClose={() => setGifOpen(false)}
|
||||
/></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<GifPicker
|
||||
apiKey={gifApiKey}
|
||||
onSelect={handleGifSelect}
|
||||
requestClose={() => setGifOpen(false)}
|
||||
/>
|
||||
</React.Suspense>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
@@ -798,7 +813,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
</UseStateProvider>
|
||||
)}
|
||||
{gifError && (
|
||||
<Text size="T100" style={{ color: 'var(--tc-danger-normal)', padding: '2px 6px', alignSelf: 'center', whiteSpace: 'nowrap' }}>
|
||||
<Text
|
||||
size="T100"
|
||||
style={{
|
||||
color: 'var(--tc-danger-normal)',
|
||||
padding: '2px 6px',
|
||||
alignSelf: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{gifError}
|
||||
</Text>
|
||||
)}
|
||||
@@ -811,14 +834,28 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
title="Share location"
|
||||
>
|
||||
{locating ? (
|
||||
<Text size="T200" style={{ fontWeight: 800, fontSize: '10px', letterSpacing: '0.04em', lineHeight: 1 }}>
|
||||
<Text
|
||||
size="T200"
|
||||
style={{
|
||||
fontWeight: 800,
|
||||
fontSize: '10px',
|
||||
letterSpacing: '0.04em',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
...
|
||||
</Text>
|
||||
) : (
|
||||
<Icon src={Icons.Pin} size="100" />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton onClick={submit} variant="SurfaceVariant" size="300" radii="300" aria-label="Send message">
|
||||
<IconButton
|
||||
onClick={submit}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<Icon src={Icons.Send} />
|
||||
</IconButton>
|
||||
</>
|
||||
|
||||
@@ -637,7 +637,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
// and either there are no unread messages or the latest message is from the current user.
|
||||
// If either condition is met, trigger the markAsRead function to send a read receipt.
|
||||
const _roomId = mEvt.getRoomId();
|
||||
if (_roomId) requestAnimationFrame(() => markAsRead(mx, _roomId, hideActivity));
|
||||
if (_roomId) requestAnimationFrame(() => markAsRead(mx, _roomId, hideActivity));
|
||||
}
|
||||
|
||||
if (!document.hasFocus() && !unreadInfo) {
|
||||
@@ -673,7 +673,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
) => {
|
||||
const evtTimeline = getEventTimeline(room, evtId);
|
||||
const absoluteIndex =
|
||||
evtTimeline && getEventIdAbsoluteIndex(timelineRef.current.linkedTimelines, evtTimeline, evtId);
|
||||
evtTimeline &&
|
||||
getEventIdAbsoluteIndex(timelineRef.current.linkedTimelines, evtTimeline, evtId);
|
||||
|
||||
if (typeof absoluteIndex === 'number') {
|
||||
const scrolled = scrollToItem(absoluteIndex, {
|
||||
@@ -1242,7 +1243,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
mEvent.getType() === 'm.poll.start' ||
|
||||
mEvent.getType() === 'org.matrix.msc3381.poll.start'
|
||||
)
|
||||
return <PollContent content={mEvent.getContent()} roomId={room.roomId} eventId={mEvent.getId() ?? undefined} />;
|
||||
return (
|
||||
<PollContent
|
||||
content={mEvent.getContent()}
|
||||
roomId={room.roomId}
|
||||
eventId={mEvent.getId() ?? undefined}
|
||||
/>
|
||||
);
|
||||
if (mEvent.getType() === MessageEvent.RoomMessageEncrypted)
|
||||
return (
|
||||
<Text>
|
||||
@@ -1371,7 +1378,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
{mEvent.isRedacted() ? (
|
||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||
) : (
|
||||
<PollContent content={mEvent.getContent()} roomId={room.roomId} eventId={mEvent.getId() ?? undefined} />
|
||||
<PollContent
|
||||
content={mEvent.getContent()}
|
||||
roomId={room.roomId}
|
||||
eventId={mEvent.getId() ?? undefined}
|
||||
/>
|
||||
)}
|
||||
</Message>
|
||||
);
|
||||
@@ -1424,7 +1435,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
{mEvent.isRedacted() ? (
|
||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||
) : (
|
||||
<PollContent content={mEvent.getContent()} roomId={room.roomId} eventId={mEvent.getId() ?? undefined} />
|
||||
<PollContent
|
||||
content={mEvent.getContent()}
|
||||
roomId={room.roomId}
|
||||
eventId={mEvent.getId() ?? undefined}
|
||||
/>
|
||||
)}
|
||||
</Message>
|
||||
);
|
||||
@@ -1608,12 +1623,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
|
||||
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Lock}
|
||||
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{' enabled end-to-end encryption'}</Text></Box>}
|
||||
<Event
|
||||
key={mEvent.getId()}
|
||||
data-message-item={item}
|
||||
data-message-id={mEventId}
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
highlight={highlighted}
|
||||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
time={timeJSX}
|
||||
iconSrc={Icons.Lock}
|
||||
content={
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T300" priority="300">
|
||||
<b>{senderName}</b>
|
||||
{' enabled end-to-end encryption'}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Event>
|
||||
);
|
||||
@@ -1623,14 +1664,45 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const senderId = mEvent.getSender() ?? '';
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
const joinRule = mEvent.getContent<{ join_rule?: string }>().join_rule ?? 'unknown';
|
||||
const ruleLabel: Record<string, string> = { public: 'public', invite: 'invite-only', knock: 'knock', restricted: 'restricted' };
|
||||
const ruleLabel: Record<string, string> = {
|
||||
public: 'public',
|
||||
invite: 'invite-only',
|
||||
knock: 'knock',
|
||||
restricted: 'restricted',
|
||||
};
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
|
||||
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Settings}
|
||||
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{` set room join rule to ${ruleLabel[joinRule] ?? joinRule}`}</Text></Box>}
|
||||
<Event
|
||||
key={mEvent.getId()}
|
||||
data-message-item={item}
|
||||
data-message-id={mEventId}
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
highlight={highlighted}
|
||||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
time={timeJSX}
|
||||
iconSrc={Icons.Settings}
|
||||
content={
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T300" priority="300">
|
||||
<b>{senderName}</b>
|
||||
{` set room join rule to ${ruleLabel[joinRule] ?? joinRule}`}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Event>
|
||||
);
|
||||
@@ -1641,12 +1713,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
const access = mEvent.getContent<{ guest_access?: string }>().guest_access ?? 'unknown';
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
|
||||
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Settings}
|
||||
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{access === 'can_join' ? ' allowed guest access' : ' disabled guest access'}</Text></Box>}
|
||||
<Event
|
||||
key={mEvent.getId()}
|
||||
data-message-item={item}
|
||||
data-message-id={mEventId}
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
highlight={highlighted}
|
||||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
time={timeJSX}
|
||||
iconSrc={Icons.Settings}
|
||||
content={
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T300" priority="300">
|
||||
<b>{senderName}</b>
|
||||
{access === 'can_join' ? ' allowed guest access' : ' disabled guest access'}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Event>
|
||||
);
|
||||
@@ -1657,12 +1755,38 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
const senderName = getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId);
|
||||
const alias = mEvent.getContent<{ alias?: string }>().alias;
|
||||
const timeJSX = (
|
||||
<Time ts={mEvent.getTs()} compact={messageLayout === MessageLayout.Compact} hour24Clock={hour24Clock} dateFormatString={dateFormatString} />
|
||||
<Time
|
||||
ts={mEvent.getTs()}
|
||||
compact={messageLayout === MessageLayout.Compact}
|
||||
hour24Clock={hour24Clock}
|
||||
dateFormatString={dateFormatString}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Event key={mEvent.getId()} data-message-item={item} data-message-id={mEventId} room={room} mEvent={mEvent} highlight={highlighted} messageSpacing={messageSpacing} canDelete={canRedact || mEvent.getSender() === mx.getUserId()} hideReadReceipts={hideActivity} showDeveloperTools={showDeveloperTools}>
|
||||
<EventContent messageLayout={messageLayout} time={timeJSX} iconSrc={Icons.Hash}
|
||||
content={<Box grow="Yes" direction="Column"><Text size="T300" priority="300"><b>{senderName}</b>{alias ? ` set room address to ${alias}` : ' removed room address'}</Text></Box>}
|
||||
<Event
|
||||
key={mEvent.getId()}
|
||||
data-message-item={item}
|
||||
data-message-id={mEventId}
|
||||
room={room}
|
||||
mEvent={mEvent}
|
||||
highlight={highlighted}
|
||||
messageSpacing={messageSpacing}
|
||||
canDelete={canRedact || mEvent.getSender() === mx.getUserId()}
|
||||
hideReadReceipts={hideActivity}
|
||||
showDeveloperTools={showDeveloperTools}
|
||||
>
|
||||
<EventContent
|
||||
messageLayout={messageLayout}
|
||||
time={timeJSX}
|
||||
iconSrc={Icons.Hash}
|
||||
content={
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Text size="T300" priority="300">
|
||||
<b>{senderName}</b>
|
||||
{alias ? ` set room address to ${alias}` : ' removed room address'}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Event>
|
||||
);
|
||||
@@ -1831,9 +1955,15 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
const [base, len] = timelineSegments[mid];
|
||||
if (item < base) { hi = mid - 1; }
|
||||
else if (item >= base + len) { lo = mid + 1; }
|
||||
else { eventTimeline = timelineSegments[mid][2]; baseIndex = base; break; }
|
||||
if (item < base) {
|
||||
hi = mid - 1;
|
||||
} else if (item >= base + len) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
eventTimeline = timelineSegments[mid][2];
|
||||
baseIndex = base;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!eventTimeline) return null;
|
||||
@@ -1935,131 +2065,131 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||
|
||||
return (
|
||||
<ReadPositionsContext.Provider value={readPositions}>
|
||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
|
||||
<TimelineFloat position="Top">
|
||||
<Chip
|
||||
variant="Primary"
|
||||
radii="Pill"
|
||||
outlined
|
||||
before={<Icon size="50" src={Icons.MessageUnread} />}
|
||||
onClick={handleJumpToUnread}
|
||||
>
|
||||
<Text size="L400">Jump to Unread</Text>
|
||||
</Chip>
|
||||
|
||||
<Chip
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
outlined
|
||||
before={<Icon size="50" src={Icons.CheckTwice} />}
|
||||
onClick={handleMarkAsRead}
|
||||
>
|
||||
<Text size="L400">Mark as Read</Text>
|
||||
</Chip>
|
||||
</TimelineFloat>
|
||||
)}
|
||||
<Scroll ref={scrollRef} visibility="Hover">
|
||||
<Box
|
||||
direction="Column"
|
||||
justifyContent="End"
|
||||
style={{ minHeight: '100%', padding: `${config.space.S600} 0` }}
|
||||
>
|
||||
{!canPaginateBack && rangeAtStart && getItems().length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${
|
||||
messageLayout === MessageLayout.Compact ? config.space.S400 : toRem(64)
|
||||
}`,
|
||||
}}
|
||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
|
||||
<TimelineFloat position="Top">
|
||||
<Chip
|
||||
variant="Primary"
|
||||
radii="Pill"
|
||||
outlined
|
||||
before={<Icon size="50" src={Icons.MessageUnread} />}
|
||||
onClick={handleJumpToUnread}
|
||||
>
|
||||
<RoomIntro room={room} />
|
||||
</div>
|
||||
)}
|
||||
{(canPaginateBack || !rangeAtStart) &&
|
||||
(messageLayout === MessageLayout.Compact ? (
|
||||
<>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase ref={observeBackAnchor}>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MessageBase>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase ref={observeBackAnchor}>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
</>
|
||||
))}
|
||||
<Text size="L400">Jump to Unread</Text>
|
||||
</Chip>
|
||||
|
||||
{getItems().map(eventRenderer)}
|
||||
|
||||
{(!liveTimelineLinked || !rangeAtEnd) &&
|
||||
(messageLayout === MessageLayout.Compact ? (
|
||||
<>
|
||||
<MessageBase ref={observeFrontAnchor}>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MessageBase ref={observeFrontAnchor}>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
</>
|
||||
))}
|
||||
<span ref={atBottomAnchorRef} />
|
||||
</Box>
|
||||
</Scroll>
|
||||
{!atBottom && (
|
||||
<TimelineFloat position="Bottom">
|
||||
<Chip
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
outlined
|
||||
before={<Icon size="50" src={Icons.ArrowBottom} />}
|
||||
onClick={handleJumpToLatest}
|
||||
<Chip
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
outlined
|
||||
before={<Icon size="50" src={Icons.CheckTwice} />}
|
||||
onClick={handleMarkAsRead}
|
||||
>
|
||||
<Text size="L400">Mark as Read</Text>
|
||||
</Chip>
|
||||
</TimelineFloat>
|
||||
)}
|
||||
<Scroll ref={scrollRef} visibility="Hover">
|
||||
<Box
|
||||
direction="Column"
|
||||
justifyContent="End"
|
||||
style={{ minHeight: '100%', padding: `${config.space.S600} 0` }}
|
||||
>
|
||||
<Text size="L400">Jump to Latest</Text>
|
||||
</Chip>
|
||||
</TimelineFloat>
|
||||
)}
|
||||
</Box>
|
||||
{!canPaginateBack && rangeAtStart && getItems().length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
padding: `${config.space.S700} ${config.space.S400} ${config.space.S600} ${
|
||||
messageLayout === MessageLayout.Compact ? config.space.S400 : toRem(64)
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
<RoomIntro room={room} />
|
||||
</div>
|
||||
)}
|
||||
{(canPaginateBack || !rangeAtStart) &&
|
||||
(messageLayout === MessageLayout.Compact ? (
|
||||
<>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase ref={observeBackAnchor}>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MessageBase>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase ref={observeBackAnchor}>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
</>
|
||||
))}
|
||||
|
||||
{getItems().map(eventRenderer)}
|
||||
|
||||
{(!liveTimelineLinked || !rangeAtEnd) &&
|
||||
(messageLayout === MessageLayout.Compact ? (
|
||||
<>
|
||||
<MessageBase ref={observeFrontAnchor}>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<CompactPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MessageBase ref={observeFrontAnchor}>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
<MessageBase>
|
||||
<DefaultPlaceholder key={getItems().length} />
|
||||
</MessageBase>
|
||||
</>
|
||||
))}
|
||||
<span ref={atBottomAnchorRef} />
|
||||
</Box>
|
||||
</Scroll>
|
||||
{!atBottom && (
|
||||
<TimelineFloat position="Bottom">
|
||||
<Chip
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
outlined
|
||||
before={<Icon size="50" src={Icons.ArrowBottom} />}
|
||||
onClick={handleJumpToLatest}
|
||||
>
|
||||
<Text size="L400">Jump to Latest</Text>
|
||||
</Chip>
|
||||
</TimelineFloat>
|
||||
)}
|
||||
</Box>
|
||||
</ReadPositionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,8 +56,6 @@ const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export function RoomView({ eventId }: { eventId?: string }) {
|
||||
const roomInputRef = useRef<HTMLDivElement>(null);
|
||||
const roomViewRef = useRef<HTMLDivElement>(null);
|
||||
@@ -98,8 +96,9 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||
)
|
||||
);
|
||||
|
||||
const chatBgStyle = useMemo(
|
||||
() => getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark),
|
||||
const chatBgStyle = useMemo(
|
||||
() =>
|
||||
getChatBg(lotusTerminal && chatBackground === 'none' ? 'tactical' : chatBackground, isDark),
|
||||
[chatBackground, lotusTerminal, isDark]
|
||||
);
|
||||
|
||||
|
||||
@@ -533,7 +533,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton fill="None" ref={triggerRef} onClick={handleSearchClick} aria-label="Search">
|
||||
<IconButton
|
||||
fill="None"
|
||||
ref={triggerRef}
|
||||
onClick={handleSearchClick}
|
||||
aria-label="Search"
|
||||
>
|
||||
<Icon size="400" src={Icons.Search} />
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -596,11 +601,13 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
{!room.isCallRoom() && livekitSupported && rtcSupported && hasCallPermission &&
|
||||
(direct || (room.getJoinRule() === 'invite' &&
|
||||
getStateEvents(room, StateEvent.SpaceParent).length === 0)) && (
|
||||
<CallButton />
|
||||
)}
|
||||
{!room.isCallRoom() &&
|
||||
livekitSupported &&
|
||||
rtcSupported &&
|
||||
hasCallPermission &&
|
||||
(direct ||
|
||||
(room.getJoinRule() === 'invite' &&
|
||||
getStateEvents(room, StateEvent.SpaceParent).length === 0)) && <CallButton />}
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
@@ -616,7 +623,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton fill="None" ref={triggerRef} onClick={handleMemberToggle} aria-label="Toggle member list">
|
||||
<IconButton
|
||||
fill="None"
|
||||
ref={triggerRef}
|
||||
onClick={handleMemberToggle}
|
||||
aria-label="Toggle member list"
|
||||
>
|
||||
<Icon size="400" src={Icons.User} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -117,7 +117,13 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<IconButton title="Drop Typing Status" aria-label="Drop typing status" size="300" radii="Pill" onClick={handleDropAll}>
|
||||
<IconButton
|
||||
title="Drop Typing Status"
|
||||
aria-label="Drop typing status"
|
||||
size="300"
|
||||
radii="Pill"
|
||||
onClick={handleDropAll}
|
||||
>
|
||||
<Icon size="50" src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -57,7 +57,12 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
>
|
||||
<Modal
|
||||
size="400"
|
||||
style={{ maxHeight: '440px', borderRadius: config.radii.R500, display: 'flex', flexDirection: 'column' }}
|
||||
style={{
|
||||
maxHeight: '440px',
|
||||
borderRadius: config.radii.R500,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
direction="Column"
|
||||
@@ -89,11 +94,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||
) : (
|
||||
<Box grow="Yes" style={{ minHeight: 0 }}>
|
||||
<Scroll size="300" hideTrack visibility="Hover">
|
||||
<Box
|
||||
direction="Column"
|
||||
gap="100"
|
||||
style={{ padding: config.space.S200 }}
|
||||
>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||
{filtered.slice(0, 60).map((room) => (
|
||||
<MenuItem
|
||||
key={room.roomId}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -141,7 +141,11 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<IconButton onClick={requestClose} variant="Background" aria-label="Close settings">
|
||||
<IconButton
|
||||
onClick={requestClose}
|
||||
variant="Background"
|
||||
aria-label="Close settings"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -185,7 +185,12 @@ function ProfileAvatar({ profile, userId }: ProfileProps) {
|
||||
<Box grow="Yes">
|
||||
<Text size="H4">Remove Avatar</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={() => setAlertRemove(false)} radii="300" aria-label="Cancel">
|
||||
<IconButton
|
||||
size="300"
|
||||
onClick={() => setAlertRemove(false)}
|
||||
radii="300"
|
||||
aria-label="Cancel"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
||||
@@ -34,7 +34,13 @@ import FocusTrap from 'focus-trap-react';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { ChatBackground, DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import {
|
||||
ChatBackground,
|
||||
DateFormat,
|
||||
MessageLayout,
|
||||
MessageSpacing,
|
||||
settingsAtom,
|
||||
} from '../../../state/settings';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { KeySymbol } from '../../../utils/key-symbol';
|
||||
import { isMacOS } from '../../../utils/user-agent';
|
||||
@@ -313,7 +319,10 @@ function Appearance() {
|
||||
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
|
||||
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||
const [perMessageProfiles, setPerMessageProfiles] = useSetting(settingsAtom, 'perMessageProfiles');
|
||||
const [perMessageProfiles, setPerMessageProfiles] = useSetting(
|
||||
settingsAtom,
|
||||
'perMessageProfiles'
|
||||
);
|
||||
const [lotusTerminal, setLotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||
|
||||
return (
|
||||
@@ -359,7 +368,12 @@ function Appearance() {
|
||||
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
|
||||
</SequenceCard>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="200">
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="200"
|
||||
>
|
||||
<SettingTile
|
||||
title="Chat Background"
|
||||
description="Pattern applied behind the message timeline."
|
||||
@@ -373,7 +387,9 @@ function Appearance() {
|
||||
<SettingTile
|
||||
title="Show Profile on Every Message"
|
||||
description="Display avatar and name on each message instead of grouping consecutive messages."
|
||||
after={<Switch variant="Primary" value={perMessageProfiles} onChange={setPerMessageProfiles} />}
|
||||
after={
|
||||
<Switch variant="Primary" value={perMessageProfiles} onChange={setPerMessageProfiles} />
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
@@ -385,7 +401,10 @@ function Appearance() {
|
||||
{lotusTerminal && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { resetBootSequence(); runLotusBootSequence(true); }}
|
||||
onClick={() => {
|
||||
resetBootSequence();
|
||||
runLotusBootSequence(true);
|
||||
}}
|
||||
title="Replay boot sequence"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
@@ -808,10 +827,12 @@ function Editor() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Calls() {
|
||||
const [cameraOnJoin, setCameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
|
||||
const [callNoiseSuppression, setCallNoiseSuppression] = useSetting(settingsAtom, 'callNoiseSuppression');
|
||||
const [callNoiseSuppression, setCallNoiseSuppression] = useSetting(
|
||||
settingsAtom,
|
||||
'callNoiseSuppression'
|
||||
);
|
||||
const [pttMode, setPttMode] = useSetting(settingsAtom, 'pttMode');
|
||||
const [pttKey, setPttKey] = useSetting(settingsAtom, 'pttKey');
|
||||
const [listeningForKey, setListeningForKey] = useState(false);
|
||||
@@ -843,7 +864,8 @@ function Calls() {
|
||||
window.addEventListener('keydown', onKey, true);
|
||||
}, [listeningForKey, setPttKey]);
|
||||
|
||||
const keyLabel = (code: string) => code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', '');
|
||||
const keyLabel = (code: string) =>
|
||||
code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', '');
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
@@ -859,10 +881,21 @@ function Calls() {
|
||||
<SettingTile
|
||||
title="Noise Suppression"
|
||||
description="Apply AI noise suppression to filter background noise during calls (powered by Element Call)."
|
||||
after={<Switch variant="Primary" value={callNoiseSuppression} onChange={setCallNoiseSuppression} />}
|
||||
after={
|
||||
<Switch
|
||||
variant="Primary"
|
||||
value={callNoiseSuppression}
|
||||
onChange={setCallNoiseSuppression}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column" gap="400">
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
variant="SurfaceVariant"
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Push to Talk"
|
||||
description="Mute your microphone by default. Hold the PTT key to speak."
|
||||
@@ -882,9 +915,7 @@ function Calls() {
|
||||
onClick={handleKeyBind}
|
||||
style={{ minWidth: '90px' }}
|
||||
>
|
||||
<Text size="B300">
|
||||
{listeningForKey ? 'Press a key…' : keyLabel(pttKey)}
|
||||
</Text>
|
||||
<Text size="B300">{listeningForKey ? 'Press a key…' : keyLabel(pttKey)}</Text>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@@ -894,7 +925,6 @@ function Calls() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ChatBgGrid() {
|
||||
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const theme = useTheme();
|
||||
@@ -915,18 +945,16 @@ function ChatBgGrid() {
|
||||
height: toRem(50),
|
||||
borderRadius: toRem(8),
|
||||
cursor: 'pointer',
|
||||
border: chatBackground === opt.value
|
||||
? '2px solid #980000'
|
||||
: '2px solid rgba(128,128,128,0.25)',
|
||||
border:
|
||||
chatBackground === opt.value
|
||||
? '2px solid #980000'
|
||||
: '2px solid rgba(128,128,128,0.25)',
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
...getChatBg(opt.value as ChatBackground, isDark),
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="T200"
|
||||
style={chatBackground === opt.value ? { color: '#980000' } : undefined}
|
||||
>
|
||||
<Text size="T200" style={chatBackground === opt.value ? { color: '#980000' } : undefined}>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -120,7 +120,14 @@ function KeywordCross({ pushRule }: PushRulesProps) {
|
||||
|
||||
const removing = removeState.status === AsyncStatus.Loading;
|
||||
return (
|
||||
<IconButton onClick={remove} size="300" radii="Pill" variant="Secondary" disabled={removing} aria-label="Remove keyword">
|
||||
<IconButton
|
||||
onClick={remove}
|
||||
size="300"
|
||||
radii="Pill"
|
||||
variant="Secondary"
|
||||
disabled={removing}
|
||||
aria-label="Remove keyword"
|
||||
>
|
||||
{removing ? <Spinner size="100" /> : <Icon src={Icons.Cross} size="100" />}
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
@@ -117,7 +117,11 @@ export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps)
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
{screenSize === ScreenSize.Mobile && (
|
||||
<IconButton onClick={requestClose} variant="Background" aria-label="Close settings">
|
||||
<IconButton
|
||||
onClick={requestClose}
|
||||
variant="Background"
|
||||
aria-label="Close settings"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -53,10 +53,19 @@ export const createCallEmbed = (
|
||||
MatrixRTCSession.sessionMembershipsForRoom(room, rtcSession.sessionDescription).length > 0;
|
||||
|
||||
const intent = CallEmbed.getIntent(dm, ongoing, pref?.video);
|
||||
const initialAudio = forceAudioOff ? false : (pref?.microphone ?? true);
|
||||
const initialAudio = forceAudioOff ? false : pref?.microphone ?? true;
|
||||
const initialVideo = pref?.video ?? false;
|
||||
const widget = CallEmbed.getWidget(mx, room, intent, themeKind, noiseSuppression, initialAudio, initialVideo);
|
||||
const controlState = pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
||||
const widget = CallEmbed.getWidget(
|
||||
mx,
|
||||
room,
|
||||
intent,
|
||||
themeKind,
|
||||
noiseSuppression,
|
||||
initialAudio,
|
||||
initialVideo
|
||||
);
|
||||
const controlState =
|
||||
pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);
|
||||
|
||||
const embed = new CallEmbed(mx, room, widget, container, controlState);
|
||||
|
||||
@@ -77,7 +86,16 @@ export const useCallStart = (dm = false) => {
|
||||
if (!container) {
|
||||
throw new Error('Failed to start call, No embed container element found!');
|
||||
}
|
||||
const callEmbed = createCallEmbed(mx, room, dm, theme.kind, container, pref, callNoiseSuppression ?? true, !!pttMode);
|
||||
const callEmbed = createCallEmbed(
|
||||
mx,
|
||||
room,
|
||||
dm,
|
||||
theme.kind,
|
||||
container,
|
||||
pref,
|
||||
callNoiseSuppression ?? true,
|
||||
!!pttMode
|
||||
);
|
||||
|
||||
setCallEmbed(callEmbed);
|
||||
},
|
||||
|
||||
@@ -4,7 +4,10 @@ import { useState } from 'react';
|
||||
export function useForceUpdate() {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
return [data, function forceUpdateHook() {
|
||||
setData({});
|
||||
}];
|
||||
return [
|
||||
data,
|
||||
function forceUpdateHook() {
|
||||
setData({});
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -55,11 +55,14 @@ export const usePan = (active: boolean) => {
|
||||
}, [active]);
|
||||
|
||||
// Clean up document listeners if component unmounts during an active drag
|
||||
useEffect(() => () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
useEffect(
|
||||
() => () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
pan,
|
||||
|
||||
@@ -23,9 +23,7 @@ function computePositions(room: Room, myUserId: string): Map<string, string[]> {
|
||||
const map = new Map<string, string[]>();
|
||||
const liveEvents = room.getLiveTimeline().getEvents();
|
||||
// Build O(1) index once instead of O(T) findIndex per member
|
||||
const eventIndex = new Map<string, number>(
|
||||
liveEvents.map((e, i) => [e.getId() ?? '', i])
|
||||
);
|
||||
const eventIndex = new Map<string, number>(liveEvents.map((e, i) => [e.getId() ?? '', i]));
|
||||
for (const member of room.getJoinedMembers()) {
|
||||
if (member.userId === myUserId) continue;
|
||||
const evtId = room.getEventReadUpTo(member.userId);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { lightTheme } from 'folds';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||
import { butterTheme, darkTheme, lotusTerminalLightTheme, lotusTerminalTheme, silverTheme } from '../../colors.css';
|
||||
import {
|
||||
butterTheme,
|
||||
darkTheme,
|
||||
lotusTerminalLightTheme,
|
||||
lotusTerminalTheme,
|
||||
silverTheme,
|
||||
} from '../../colors.css';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
|
||||
@@ -45,7 +51,12 @@ export const LotusTerminalTheme: Theme = {
|
||||
export const LotusTerminalLightTheme: Theme = {
|
||||
id: 'lotus-terminal-light-theme',
|
||||
kind: ThemeKind.Light,
|
||||
classNames: ['lotus-terminal-light-theme', lotusTerminalLightTheme, onLightFontWeight, 'prism-light'],
|
||||
classNames: [
|
||||
'lotus-terminal-light-theme',
|
||||
lotusTerminalLightTheme,
|
||||
onLightFontWeight,
|
||||
'prism-light',
|
||||
],
|
||||
};
|
||||
|
||||
export const useThemes = (): Theme[] => {
|
||||
|
||||
+120
-33
@@ -10,10 +10,12 @@ import {
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { ClientConfig } from '../hooks/useClientConfig';
|
||||
const AuthLayout = React.lazy(() => import('./auth').then(m => ({ default: m.AuthLayout })));
|
||||
const Login = React.lazy(() => import('./auth').then(m => ({ default: m.Login })));
|
||||
const Register = React.lazy(() => import('./auth').then(m => ({ default: m.Register })));
|
||||
const ResetPassword = React.lazy(() => import('./auth').then(m => ({ default: m.ResetPassword })));
|
||||
const AuthLayout = React.lazy(() => import('./auth').then((m) => ({ default: m.AuthLayout })));
|
||||
const Login = React.lazy(() => import('./auth').then((m) => ({ default: m.Login })));
|
||||
const Register = React.lazy(() => import('./auth').then((m) => ({ default: m.Register })));
|
||||
const ResetPassword = React.lazy(() =>
|
||||
import('./auth').then((m) => ({ default: m.ResetPassword }))
|
||||
);
|
||||
import {
|
||||
DIRECT_PATH,
|
||||
EXPLORE_PATH,
|
||||
@@ -48,9 +50,8 @@ import { Home, HomeRouteRoomProvider, HomeSearch } from './client/home';
|
||||
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
|
||||
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
|
||||
|
||||
|
||||
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
|
||||
const Room = React.lazy(() => import('../features/room').then(m => ({ default: m.Room })));
|
||||
const Room = React.lazy(() => import('../features/room').then((m) => ({ default: m.Room })));
|
||||
|
||||
import { WelcomePage } from './client/WelcomePage';
|
||||
import { SidebarNav } from './client/SidebarNav';
|
||||
@@ -62,22 +63,38 @@ import { ClientNonUIFeatures } from './client/ClientNonUIFeatures';
|
||||
import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
|
||||
import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification';
|
||||
import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
|
||||
const RoomSettingsRenderer = React.lazy(() => import('../features/room-settings').then(m => ({ default: m.RoomSettingsRenderer })));
|
||||
const RoomSettingsRenderer = React.lazy(() =>
|
||||
import('../features/room-settings').then((m) => ({ default: m.RoomSettingsRenderer }))
|
||||
);
|
||||
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
|
||||
const SpaceSettingsRenderer = React.lazy(() => import('../features/space-settings').then(m => ({ default: m.SpaceSettingsRenderer })));
|
||||
const SpaceSettingsRenderer = React.lazy(() =>
|
||||
import('../features/space-settings').then((m) => ({ default: m.SpaceSettingsRenderer }))
|
||||
);
|
||||
import { UserRoomProfileRenderer } from '../components/UserRoomProfileRenderer';
|
||||
const CreateRoomModalRenderer = React.lazy(() => import('../features/create-room').then(m => ({ default: m.CreateRoomModalRenderer })));
|
||||
const CreateRoomModalRenderer = React.lazy(() =>
|
||||
import('../features/create-room').then((m) => ({ default: m.CreateRoomModalRenderer }))
|
||||
);
|
||||
import { HomeCreateRoom } from './client/home/CreateRoom';
|
||||
import { Create } from './client/create';
|
||||
const CreateSpaceModalRenderer = React.lazy(() => import('../features/create-space').then(m => ({ default: m.CreateSpaceModalRenderer })));
|
||||
const SearchModalRenderer = React.lazy(() => import('../features/search').then(m => ({ default: m.SearchModalRenderer })));
|
||||
const Explore = React.lazy(() => import('./client/explore').then(m => ({ default: m.Explore })));
|
||||
const FeaturedRooms = React.lazy(() => import('./client/explore').then(m => ({ default: m.FeaturedRooms })));
|
||||
const PublicRooms = React.lazy(() => import('./client/explore').then(m => ({ default: m.PublicRooms })));
|
||||
const Notifications = React.lazy(() => import('./client/inbox').then(m => ({ default: m.Notifications })));
|
||||
const Inbox = React.lazy(() => import('./client/inbox').then(m => ({ default: m.Inbox })));
|
||||
const Invites = React.lazy(() => import('./client/inbox').then(m => ({ default: m.Invites })));
|
||||
const Lobby = React.lazy(() => import('../features/lobby').then(m => ({ default: m.Lobby })));
|
||||
const CreateSpaceModalRenderer = React.lazy(() =>
|
||||
import('../features/create-space').then((m) => ({ default: m.CreateSpaceModalRenderer }))
|
||||
);
|
||||
const SearchModalRenderer = React.lazy(() =>
|
||||
import('../features/search').then((m) => ({ default: m.SearchModalRenderer }))
|
||||
);
|
||||
const Explore = React.lazy(() => import('./client/explore').then((m) => ({ default: m.Explore })));
|
||||
const FeaturedRooms = React.lazy(() =>
|
||||
import('./client/explore').then((m) => ({ default: m.FeaturedRooms }))
|
||||
);
|
||||
const PublicRooms = React.lazy(() =>
|
||||
import('./client/explore').then((m) => ({ default: m.PublicRooms }))
|
||||
);
|
||||
const Notifications = React.lazy(() =>
|
||||
import('./client/inbox').then((m) => ({ default: m.Notifications }))
|
||||
);
|
||||
const Inbox = React.lazy(() => import('./client/inbox').then((m) => ({ default: m.Inbox })));
|
||||
const Invites = React.lazy(() => import('./client/inbox').then((m) => ({ default: m.Invites })));
|
||||
const Lobby = React.lazy(() => import('../features/lobby').then((m) => ({ default: m.Lobby })));
|
||||
import { getFallbackSession } from '../state/sessions';
|
||||
import { CallStatusRenderer } from './CallStatusRenderer';
|
||||
import { CallEmbedProvider } from '../components/CallEmbedProvider';
|
||||
@@ -112,9 +129,30 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
</React.Suspense>
|
||||
}
|
||||
>
|
||||
<Route path={LOGIN_PATH} element={<React.Suspense fallback={null}><Login /></React.Suspense>} />
|
||||
<Route path={REGISTER_PATH} element={<React.Suspense fallback={null}><Register /></React.Suspense>} />
|
||||
<Route path={RESET_PASSWORD_PATH} element={<React.Suspense fallback={null}><ResetPassword /></React.Suspense>} />
|
||||
<Route
|
||||
path={LOGIN_PATH}
|
||||
element={
|
||||
<React.Suspense fallback={null}>
|
||||
<Login />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={REGISTER_PATH}
|
||||
element={
|
||||
<React.Suspense fallback={null}>
|
||||
<Register />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={RESET_PASSWORD_PATH}
|
||||
element={
|
||||
<React.Suspense fallback={null}>
|
||||
<ResetPassword />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
@@ -149,12 +187,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
</ClientLayout>
|
||||
<CallStatusRenderer />
|
||||
</CallEmbedProvider>
|
||||
<React.Suspense fallback={null}><SearchModalRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<SearchModalRenderer />
|
||||
</React.Suspense>
|
||||
<UserRoomProfileRenderer />
|
||||
<React.Suspense fallback={null}><CreateRoomModalRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}><CreateSpaceModalRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}><RoomSettingsRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}><SpaceSettingsRenderer /></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<CreateRoomModalRenderer />
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<CreateSpaceModalRenderer />
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<RoomSettingsRenderer />
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<SpaceSettingsRenderer />
|
||||
</React.Suspense>
|
||||
<ReceiveSelfDeviceVerification />
|
||||
<AutoRestoreBackupOnVerification />
|
||||
</ClientNonUIFeatures>
|
||||
@@ -250,7 +298,14 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_LOBBY_PATH} element={<React.Suspense fallback={null}><Lobby /></React.Suspense>} />
|
||||
<Route
|
||||
path={_LOBBY_PATH}
|
||||
element={
|
||||
<React.Suspense fallback={null}>
|
||||
<Lobby />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path={_SEARCH_PATH} element={<SpaceSearch />} />
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
@@ -269,7 +324,9 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={EXPLORE_PATH}>
|
||||
<React.Suspense fallback={null}><Explore /></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<Explore />
|
||||
</React.Suspense>
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
@@ -284,8 +341,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_FEATURED_PATH} element={<React.Suspense fallback={null}><FeaturedRooms /></React.Suspense>} />
|
||||
<Route path={_SERVER_PATH} element={<React.Suspense fallback={null}><PublicRooms /></React.Suspense>} />
|
||||
<Route
|
||||
path={_FEATURED_PATH}
|
||||
element={
|
||||
<React.Suspense fallback={null}>
|
||||
<FeaturedRooms />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={_SERVER_PATH}
|
||||
element={
|
||||
<React.Suspense fallback={null}>
|
||||
<PublicRooms />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route path={CREATE_PATH} element={<Create />} />
|
||||
<Route
|
||||
@@ -294,7 +365,9 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={INBOX_PATH}>
|
||||
<React.Suspense fallback={null}><Inbox /></React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<Inbox />
|
||||
</React.Suspense>
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
@@ -309,8 +382,22 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
||||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_NOTIFICATIONS_PATH} element={<React.Suspense fallback={null}><Notifications /></React.Suspense>} />
|
||||
<Route path={_INVITES_PATH} element={<React.Suspense fallback={null}><Invites /></React.Suspense>} />
|
||||
<Route
|
||||
path={_NOTIFICATIONS_PATH}
|
||||
element={
|
||||
<React.Suspense fallback={null}>
|
||||
<Notifications />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={_INVITES_PATH}
|
||||
element={
|
||||
<React.Suspense fallback={null}>
|
||||
<Invites />
|
||||
</React.Suspense>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="/*" element={<p>Page not found</p>} />
|
||||
|
||||
@@ -25,7 +25,9 @@ export function UnAuthRouteThemeManager() {
|
||||
if (lotusTerminal) {
|
||||
const isLight = systemThemeKind === ThemeKind.Light;
|
||||
document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark');
|
||||
document.body.classList.add(...(isLight ? LotusTerminalLightTheme : LotusTerminalTheme).classNames);
|
||||
document.body.classList.add(
|
||||
...(isLight ? LotusTerminalLightTheme : LotusTerminalTheme).classNames
|
||||
);
|
||||
document.body.classList.add(lotusTerminalBodyClass);
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
@@ -47,7 +49,9 @@ export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
|
||||
|
||||
const terminalIsLight = lotusTerminal && activeTheme.kind === ThemeKind.Light;
|
||||
const effectiveTheme = lotusTerminal
|
||||
? (terminalIsLight ? LotusTerminalLightTheme : LotusTerminalTheme)
|
||||
? terminalIsLight
|
||||
? LotusTerminalLightTheme
|
||||
: LotusTerminalTheme
|
||||
: activeTheme;
|
||||
|
||||
// Boot animation only fires when lotusTerminal is toggled on, not on every theme change
|
||||
|
||||
@@ -18,7 +18,13 @@ export function AuthFooter() {
|
||||
>
|
||||
v{pkg.version}
|
||||
</Text>
|
||||
<Text as="a" size="T300" href="https://matrix.lotusguild.org" target="_blank" rel="noreferrer">
|
||||
<Text
|
||||
as="a"
|
||||
size="T300"
|
||||
href="https://matrix.lotusguild.org"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Community
|
||||
</Text>
|
||||
<Text as="a" size="T300" href="https://matrix.org" target="_blank" rel="noreferrer">
|
||||
|
||||
@@ -135,7 +135,9 @@ export function AuthLayout() {
|
||||
<Header className={css.AuthHeader} size="600" variant="Surface">
|
||||
<Box grow="Yes" direction="Row" gap="300" alignItems="Center">
|
||||
<img className={css.AuthLogo} src={LotusLogo} alt="Lotus Chat Logo" />
|
||||
<Text as="h1" size="H3">Lotus Chat</Text>
|
||||
<Text as="h1" size="H3">
|
||||
Lotus Chat
|
||||
</Text>
|
||||
</Box>
|
||||
</Header>
|
||||
<Box className={css.AuthCardContent} direction="Column">
|
||||
|
||||
@@ -230,7 +230,15 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
|
||||
<Text as="label" htmlFor="passwordInput" size="L400" priority="300">
|
||||
Password
|
||||
</Text>
|
||||
<PasswordInput id="passwordInput" name="passwordInput" aria-label="Password" variant="Background" size="500" outlined required />
|
||||
<PasswordInput
|
||||
id="passwordInput"
|
||||
name="passwordInput"
|
||||
aria-label="Password"
|
||||
variant="Background"
|
||||
size="500"
|
||||
outlined
|
||||
required
|
||||
/>
|
||||
<Box alignItems="Start" justifyContent="SpaceBetween" gap="200">
|
||||
{loginState.status === AsyncStatus.Error && (
|
||||
<>
|
||||
|
||||
@@ -118,8 +118,8 @@ export const useLoginComplete = (data?: CustomLoginResponse) => {
|
||||
const afterLoginRedirectUrl = getAfterLoginRedirectPath();
|
||||
deleteAfterLoginRedirectPath();
|
||||
const _redir = afterLoginRedirectUrl;
|
||||
const _safePath = (_redir && /^\/(?!\/)/.test(_redir)) ? _redir : getHomePath();
|
||||
navigate(_safePath, { replace: true });
|
||||
const _safePath = _redir && /^\/(?!\/)/.test(_redir) ? _redir : getHomePath();
|
||||
navigate(_safePath, { replace: true });
|
||||
}
|
||||
}, [data, navigate]);
|
||||
};
|
||||
|
||||
@@ -21,14 +21,22 @@ export function ClientLayout({ nav, children }: ClientLayoutProps) {
|
||||
borderRadius: '0 0 4px 0',
|
||||
transition: 'top 0.1s',
|
||||
}}
|
||||
onFocus={(e) => { (e.currentTarget as HTMLElement).style.top = '0'; }}
|
||||
onBlur={(e) => { (e.currentTarget as HTMLElement).style.top = '-40px'; }}
|
||||
onFocus={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.top = '0';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.top = '-40px';
|
||||
}}
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
<Box grow="Yes">
|
||||
<Box shrink="No" as="nav" aria-label="Room navigation">{nav}</Box>
|
||||
<Box grow="Yes" as="main" id="main-content">{children}</Box>
|
||||
<Box shrink="No" as="nav" aria-label="Room navigation">
|
||||
{nav}
|
||||
</Box>
|
||||
<Box grow="Yes" as="main" id="main-content">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,8 @@ export function SpecVersions({ baseUrl, children }: { baseUrl: string; children:
|
||||
<Dialog>
|
||||
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
||||
<Text>
|
||||
Unable to connect to the homeserver. The homeserver or your internet connection may be down.
|
||||
Unable to connect to the homeserver. The homeserver or your internet connection
|
||||
may be down.
|
||||
</Text>
|
||||
<Button variant="Critical" onClick={retry}>
|
||||
<Text as="span" size="B400">
|
||||
|
||||
@@ -15,7 +15,15 @@ export function WelcomePage() {
|
||||
>
|
||||
<PageHeroSection>
|
||||
<PageHero
|
||||
icon={<img width="70" height="70" src={LotusLogo} alt="Lotus Chat" style={{ objectFit: "contain" }} />}
|
||||
icon={
|
||||
<img
|
||||
width="70"
|
||||
height="70"
|
||||
src={LotusLogo}
|
||||
alt="Lotus Chat"
|
||||
style={{ objectFit: 'contain' }}
|
||||
/>
|
||||
}
|
||||
title="Welcome to Lotus Chat"
|
||||
subTitle={
|
||||
<span>
|
||||
|
||||
@@ -108,7 +108,13 @@ function DirectHeader() {
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton aria-expanded={!!menuAnchor} aria-haspopup="menu" variant="Background" onClick={handleOpenMenu} aria-label="Direct messages options">
|
||||
<IconButton
|
||||
aria-expanded={!!menuAnchor}
|
||||
aria-haspopup="menu"
|
||||
variant="Background"
|
||||
onClick={handleOpenMenu}
|
||||
aria-label="Direct messages options"
|
||||
>
|
||||
<Icon src={Icons.VerticalDots} size="200" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -94,9 +94,16 @@ export function AddServer() {
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">Add Server</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Add Server
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={() => setDialog(false)} radii="300" aria-label="Close">
|
||||
<IconButton
|
||||
size="300"
|
||||
onClick={() => setDialog(false)}
|
||||
radii="300"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -110,7 +117,13 @@ export function AddServer() {
|
||||
<Text priority="400">Add server name to explore public communities.</Text>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Server Name</Text>
|
||||
<Input ref={serverInputRef} name="serverInput" aria-label="Server name" variant="Background" required />
|
||||
<Input
|
||||
ref={serverInputRef}
|
||||
name="serverInput"
|
||||
aria-label="Server name"
|
||||
variant="Background"
|
||||
required
|
||||
/>
|
||||
{exploreState.status === AsyncStatus.Error && (
|
||||
<Text style={{ color: color.Critical.Main }} size="T300">
|
||||
Failed to load public rooms. Please try again.
|
||||
|
||||
@@ -56,7 +56,9 @@ export function FeaturedRooms() {
|
||||
<Box direction="Column" gap="700">
|
||||
{spaces && spaces.length > 0 && (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text as="h2" size="H4">Featured Spaces</Text>
|
||||
<Text as="h2" size="H4">
|
||||
Featured Spaces
|
||||
</Text>
|
||||
<RoomCardGrid>
|
||||
{spaces.map((roomIdOrAlias) => (
|
||||
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
|
||||
@@ -85,7 +87,9 @@ export function FeaturedRooms() {
|
||||
)}
|
||||
{rooms && rooms.length > 0 && (
|
||||
<Box direction="Column" gap="400">
|
||||
<Text as="h3" size="H4">Featured Rooms</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Featured Rooms
|
||||
</Text>
|
||||
<RoomCardGrid>
|
||||
{rooms.map((roomIdOrAlias) => (
|
||||
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
|
||||
|
||||
@@ -537,7 +537,9 @@ export function PublicRooms() {
|
||||
{isSearch ? (
|
||||
<Text as="h3" size="H4">{`Results for "${serverSearchParams.term}"`}</Text>
|
||||
) : (
|
||||
<Text as="h3" size="H4">Popular Communities</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Popular Communities
|
||||
</Text>
|
||||
)}
|
||||
<Box gap="200">
|
||||
{roomTypeFilters.map((filter) => (
|
||||
|
||||
@@ -122,7 +122,13 @@ function HomeHeader() {
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton aria-expanded={!!menuAnchor} aria-haspopup="menu" variant="Background" onClick={handleOpenMenu} aria-label="Home options">
|
||||
<IconButton
|
||||
aria-expanded={!!menuAnchor}
|
||||
aria-haspopup="menu"
|
||||
variant="Background"
|
||||
onClick={handleOpenMenu}
|
||||
aria-label="Home options"
|
||||
>
|
||||
<Icon src={Icons.VerticalDots} size="200" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -429,7 +429,9 @@ function KnownInvites({
|
||||
}: KnownInvitesProps) {
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text as="h3" size="H4">Primary</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Primary
|
||||
</Text>
|
||||
{invites.length > 0 ? (
|
||||
<Box direction="Column" gap="100">
|
||||
{invites.map((invite) => (
|
||||
@@ -488,7 +490,9 @@ function UnknownInvites({
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Box gap="200" justifyContent="SpaceBetween" alignItems="Center">
|
||||
<Text as="h3" size="H4">Public</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Public
|
||||
</Text>
|
||||
<Box>
|
||||
{invites.length > 0 && (
|
||||
<Chip
|
||||
@@ -585,7 +589,9 @@ function SpamInvites({
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text as="h3" size="H4">Spam</Text>
|
||||
<Text as="h3" size="H4">
|
||||
Spam
|
||||
</Text>
|
||||
{invites.length > 0 ? (
|
||||
<Box direction="Column" gap="100">
|
||||
<SequenceCard
|
||||
|
||||
@@ -522,7 +522,13 @@ function OpenedSpaceFolder({ folder, onClose, children }: OpenedSpaceFolderProps
|
||||
>
|
||||
<SidebarFolderDropTarget ref={aboveTargetRef} position="Top" />
|
||||
<SidebarAvatar size="300">
|
||||
<IconButton data-id={folder.id} size="300" variant="Background" onClick={onClose} aria-label="Close folder">
|
||||
<IconButton
|
||||
data-id={folder.id}
|
||||
size="300"
|
||||
variant="Background"
|
||||
onClick={onClose}
|
||||
aria-label="Close folder"
|
||||
>
|
||||
<Icon size="400" src={Icons.ChevronTop} filled />
|
||||
</IconButton>
|
||||
</SidebarAvatar>
|
||||
|
||||
@@ -274,7 +274,13 @@ function SpaceHeader() {
|
||||
{joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />}
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton aria-expanded={!!menuAnchor} aria-haspopup="menu" variant="Background" onClick={handleOpenMenu} aria-label="Space options">
|
||||
<IconButton
|
||||
aria-expanded={!!menuAnchor}
|
||||
aria-haspopup="menu"
|
||||
variant="Background"
|
||||
onClick={handleOpenMenu}
|
||||
aria-label="Space options"
|
||||
>
|
||||
<Icon src={Icons.VerticalDots} size="200" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@@ -433,7 +439,9 @@ export function Space() {
|
||||
return false;
|
||||
}
|
||||
const showRoomAnyway =
|
||||
roomsWithUnreadSet.has(roomId) || roomId === selectedRoomId || callEmbed?.roomId === roomId;
|
||||
roomsWithUnreadSet.has(roomId) ||
|
||||
roomId === selectedRoomId ||
|
||||
callEmbed?.roomId === roomId;
|
||||
return !showRoomAnyway;
|
||||
},
|
||||
[space.roomId, closedCategories, roomsWithUnreadSet, selectedRoomId, callEmbed]
|
||||
|
||||
@@ -30,7 +30,10 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
|
||||
private get settingsButton(): HTMLElement | undefined {
|
||||
// EC 0.19.3: settings button has data-testid="settings-bottom-center"
|
||||
return (this.document?.querySelector('[data-testid="settings-bottom-center"]') as HTMLElement) ?? undefined;
|
||||
return (
|
||||
(this.document?.querySelector('[data-testid="settings-bottom-center"]') as HTMLElement) ??
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
private get reactionsButton(): HTMLElement | undefined {
|
||||
@@ -98,7 +101,13 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
}
|
||||
|
||||
public async forceState(desired: CallControlState) {
|
||||
this.state = new CallControlState(desired.microphone, desired.video, desired.sound, this.screenshare, this.spotlight);
|
||||
this.state = new CallControlState(
|
||||
desired.microphone,
|
||||
desired.video,
|
||||
desired.sound,
|
||||
this.screenshare,
|
||||
this.spotlight
|
||||
);
|
||||
await this.applyState();
|
||||
}
|
||||
|
||||
@@ -177,7 +186,9 @@ export class CallControl extends EventEmitter implements CallControlState {
|
||||
|
||||
// EC auto-switches to spotlight when screenshare starts — revert to grid
|
||||
if (!prevScreenshare && screenshare) {
|
||||
setTimeout(() => { if (this.spotlight) this.gridButton?.click(); }, 600);
|
||||
setTimeout(() => {
|
||||
if (this.spotlight) this.gridButton?.click();
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -462,7 +462,9 @@ export const getReactCustomHtmlParser = (
|
||||
{...props}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') params.handleSpoilerClick?.(e as any); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') params.handleSpoilerClick?.(e as any);
|
||||
}}
|
||||
onClick={params.handleSpoilerClick}
|
||||
className={css.Spoiler()}
|
||||
aria-label="Spoiler — click to reveal"
|
||||
|
||||
@@ -6,9 +6,9 @@ export type ListAction<T> =
|
||||
item: T | T[];
|
||||
}
|
||||
| {
|
||||
type: 'REPLACE';
|
||||
item: T;
|
||||
replacement: T;
|
||||
type: 'REPLACE';
|
||||
item: T;
|
||||
replacement: T;
|
||||
}
|
||||
| {
|
||||
type: 'DELETE';
|
||||
@@ -34,9 +34,12 @@ export const createListAtom = <T>() => {
|
||||
return;
|
||||
}
|
||||
if (action.type === 'REPLACE') {
|
||||
set(baseListAtom, items.map((item) => item === action.item ? action.replacement : item));
|
||||
set(
|
||||
baseListAtom,
|
||||
items.map((item) => (item === action.item ? action.replacement : item))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
export type TListAtom<T> = ReturnType<typeof createListAtom<T>>;
|
||||
export type TListAtom<T> = ReturnType<typeof createListAtom<T>>;
|
||||
|
||||
@@ -21,9 +21,7 @@ export type TUploadItem = {
|
||||
|
||||
export type TUploadListAtom = ReturnType<typeof createListAtom<TUploadItem>>;
|
||||
|
||||
export const roomIdToUploadItemsAtomFamily = atomFamily<string, TUploadListAtom>(
|
||||
createListAtom
|
||||
);
|
||||
export const roomIdToUploadItemsAtomFamily = atomFamily<string, TUploadListAtom>(createListAtom);
|
||||
|
||||
export const roomUploadAtomFamily = createUploadAtomFamily();
|
||||
|
||||
|
||||
@@ -9,7 +9,24 @@ export type DateFormat =
|
||||
| 'YYYY-MM-DD'
|
||||
| '';
|
||||
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
|
||||
export type ChatBackground = 'none' | 'blueprint' | 'carbon' | 'stars' | 'topographic' | 'herringbone' | 'crosshatch' | 'chevron' | 'polka' | 'triangles' | 'plaid' | 'tactical' | 'circuit' | 'hexgrid' | 'waves' | 'neon' | 'aurora';
|
||||
export type ChatBackground =
|
||||
| 'none'
|
||||
| 'blueprint'
|
||||
| 'carbon'
|
||||
| 'stars'
|
||||
| 'topographic'
|
||||
| 'herringbone'
|
||||
| 'crosshatch'
|
||||
| 'chevron'
|
||||
| 'polka'
|
||||
| 'triangles'
|
||||
| 'plaid'
|
||||
| 'tactical'
|
||||
| 'circuit'
|
||||
| 'hexgrid'
|
||||
| 'waves'
|
||||
| 'neon'
|
||||
| 'aurora';
|
||||
export enum MessageLayout {
|
||||
Modern = 0,
|
||||
Compact = 1,
|
||||
@@ -114,7 +131,11 @@ export const getSettings = (): Settings => {
|
||||
};
|
||||
|
||||
export const setSettings = (settings: Settings) => {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } catch { /* quota */ }
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch {
|
||||
/* quota */
|
||||
}
|
||||
};
|
||||
|
||||
const baseSettings = atom<Settings>(getSettings());
|
||||
|
||||
@@ -84,7 +84,15 @@ const transformFontTag: Transformer = (tagName, attribs) => ({
|
||||
tagName,
|
||||
attribs: {
|
||||
...attribs,
|
||||
style: `${attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color']) ? `background-color: ${attribs['data-mx-bg-color']};` : ''} ${attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color']) ? `color: ${attribs['data-mx-color']}` : ''}`.trim(),
|
||||
style: `${
|
||||
attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color'])
|
||||
? `background-color: ${attribs['data-mx-bg-color']};`
|
||||
: ''
|
||||
} ${
|
||||
attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color'])
|
||||
? `color: ${attribs['data-mx-color']}`
|
||||
: ''
|
||||
}`.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,7 +100,15 @@ const transformSpanTag: Transformer = (tagName, attribs) => ({
|
||||
tagName,
|
||||
attribs: {
|
||||
...attribs,
|
||||
style: `${attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color']) ? `background-color: ${attribs['data-mx-bg-color']};` : ''} ${attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color']) ? `color: ${attribs['data-mx-color']}` : ''}`.trim(),
|
||||
style: `${
|
||||
attribs['data-mx-bg-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-bg-color'])
|
||||
? `background-color: ${attribs['data-mx-bg-color']};`
|
||||
: ''
|
||||
} ${
|
||||
attribs['data-mx-color'] && /^#[0-9a-fA-F]{3,8}$/.test(attribs['data-mx-color'])
|
||||
? `color: ${attribs['data-mx-color']}`
|
||||
: ''
|
||||
}`.trim(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -421,4 +421,3 @@ export const lotusTerminalLightTheme = createTheme(color, {
|
||||
Overlay: 'rgba(237, 240, 245, 0.97)',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+197
-159
@@ -2,16 +2,16 @@ import { globalStyle, keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
const glitch1 = keyframes({
|
||||
'0%, 90%, 100%': { clipPath: 'inset(0)', transform: 'skewX(0)' },
|
||||
'92%': { clipPath: 'inset(15% 0 72% 0)', transform: 'skewX(-4deg)' },
|
||||
'94%': { clipPath: 'inset(54% 0 22% 0)', transform: 'skewX(3deg)' },
|
||||
'96%': { clipPath: 'inset(30% 0 48% 0)', transform: 'skewX(-2deg)' },
|
||||
'92%': { clipPath: 'inset(15% 0 72% 0)', transform: 'skewX(-4deg)' },
|
||||
'94%': { clipPath: 'inset(54% 0 22% 0)', transform: 'skewX(3deg)' },
|
||||
'96%': { clipPath: 'inset(30% 0 48% 0)', transform: 'skewX(-2deg)' },
|
||||
});
|
||||
|
||||
const glitch2 = keyframes({
|
||||
'0%, 90%, 100%': { clipPath: 'inset(0)', transform: 'skewX(0)' },
|
||||
'92%': { clipPath: 'inset(60% 0 8% 0)', transform: 'skewX(3deg)' },
|
||||
'94%': { clipPath: 'inset(8% 0 68% 0)', transform: 'skewX(-3deg)' },
|
||||
'96%': { clipPath: 'inset(42% 0 38% 0)', transform: 'skewX(1deg)' },
|
||||
'92%': { clipPath: 'inset(60% 0 8% 0)', transform: 'skewX(3deg)' },
|
||||
'94%': { clipPath: 'inset(8% 0 68% 0)', transform: 'skewX(-3deg)' },
|
||||
'96%': { clipPath: 'inset(42% 0 38% 0)', transform: 'skewX(1deg)' },
|
||||
});
|
||||
|
||||
export const lotusTerminalBodyClass = style({
|
||||
@@ -23,67 +23,67 @@ export const lotusTerminalBodyClass = style({
|
||||
backgroundSize: '28px 28px',
|
||||
vars: {
|
||||
// Backgrounds
|
||||
'--lt-bg-primary': '#030508',
|
||||
'--lt-bg-primary': '#030508',
|
||||
'--lt-bg-secondary': '#060c14',
|
||||
'--lt-bg-tertiary': '#0d1520',
|
||||
'--lt-bg-card': '#07101a',
|
||||
'--lt-bg-terminal': '#010304',
|
||||
'--lt-bg-tertiary': '#0d1520',
|
||||
'--lt-bg-card': '#07101a',
|
||||
'--lt-bg-terminal': '#010304',
|
||||
// Accent — Orange
|
||||
'--lt-accent-orange': '#FF6B00',
|
||||
'--lt-accent-orange': '#FF6B00',
|
||||
'--lt-accent-orange-bright': '#FF8C2B',
|
||||
'--lt-accent-orange-dim': 'rgba(255,107,0,0.12)',
|
||||
'--lt-accent-orange-dim': 'rgba(255,107,0,0.12)',
|
||||
'--lt-accent-orange-border': 'rgba(255,107,0,0.35)',
|
||||
// Accent — Amber
|
||||
'--lt-accent-amber': '#FFB300',
|
||||
'--lt-accent-amber': '#FFB300',
|
||||
'--lt-accent-amber-dim': 'rgba(255,179,0,0.10)',
|
||||
// Accent — Cyan
|
||||
'--lt-accent-cyan': '#00D4FF',
|
||||
'--lt-accent-cyan': '#00D4FF',
|
||||
'--lt-accent-cyan-bright': '#33DFFF',
|
||||
'--lt-accent-cyan-dim': 'rgba(0,212,255,0.10)',
|
||||
'--lt-accent-cyan-dim': 'rgba(0,212,255,0.10)',
|
||||
'--lt-accent-cyan-border': 'rgba(0,212,255,0.22)',
|
||||
// Accent — Green
|
||||
'--lt-accent-green': '#00FF88',
|
||||
'--lt-accent-green': '#00FF88',
|
||||
'--lt-accent-green-bright': '#33FFAA',
|
||||
'--lt-accent-green-dim': 'rgba(0,255,136,0.10)',
|
||||
'--lt-accent-green-dim': 'rgba(0,255,136,0.10)',
|
||||
'--lt-accent-green-border': 'rgba(0,255,136,0.22)',
|
||||
// Accent — Red
|
||||
'--lt-accent-red': '#FF2D55',
|
||||
'--lt-accent-red': '#FF2D55',
|
||||
'--lt-accent-red-dim': 'rgba(255,45,85,0.12)',
|
||||
// Accent — Gold
|
||||
'--lt-accent-gold': '#FFD700',
|
||||
'--lt-accent-gold': '#FFD700',
|
||||
'--lt-accent-gold-dim': 'rgba(255,215,0,0.10)',
|
||||
// Accent — Purple
|
||||
'--lt-accent-purple': '#BF5FFF',
|
||||
'--lt-accent-purple': '#BF5FFF',
|
||||
'--lt-accent-purple-dim': 'rgba(191,95,255,0.10)',
|
||||
// Text
|
||||
'--lt-text-primary': '#c4d9ee',
|
||||
'--lt-text-primary': '#c4d9ee',
|
||||
'--lt-text-secondary': '#7fa3bf',
|
||||
'--lt-text-muted': '#3e607a',
|
||||
'--lt-text-dim': '#1e3347',
|
||||
'--lt-text-muted': '#3e607a',
|
||||
'--lt-text-dim': '#1e3347',
|
||||
// Borders
|
||||
'--lt-border-color': 'rgba(0,212,255,0.16)',
|
||||
'--lt-border-color-hi': '#00D4FF',
|
||||
'--lt-border-color': 'rgba(0,212,255,0.16)',
|
||||
'--lt-border-color-hi': '#00D4FF',
|
||||
'--lt-border-color-dim': 'rgba(0,212,255,0.07)',
|
||||
// Glows — text
|
||||
'--lt-glow-orange': '0 0 6px #FF6B00, 0 0 16px rgba(255,107,0,0.55)',
|
||||
'--lt-glow-orange-intense':'0 0 8px #FF6B00, 0 0 22px #FF6B00, 0 0 40px rgba(255,107,0,0.45)',
|
||||
'--lt-glow-cyan': '0 0 6px #00D4FF, 0 0 16px rgba(0,212,255,0.45)',
|
||||
'--lt-glow-cyan-intense': '0 0 8px #00D4FF, 0 0 22px #00D4FF, 0 0 38px rgba(0,212,255,0.35)',
|
||||
'--lt-glow-green': '0 0 6px #00FF88, 0 0 16px rgba(0,255,136,0.45)',
|
||||
'--lt-glow-orange': '0 0 6px #FF6B00, 0 0 16px rgba(255,107,0,0.55)',
|
||||
'--lt-glow-orange-intense': '0 0 8px #FF6B00, 0 0 22px #FF6B00, 0 0 40px rgba(255,107,0,0.45)',
|
||||
'--lt-glow-cyan': '0 0 6px #00D4FF, 0 0 16px rgba(0,212,255,0.45)',
|
||||
'--lt-glow-cyan-intense': '0 0 8px #00D4FF, 0 0 22px #00D4FF, 0 0 38px rgba(0,212,255,0.35)',
|
||||
'--lt-glow-green': '0 0 6px #00FF88, 0 0 16px rgba(0,255,136,0.45)',
|
||||
'--lt-glow-green-intense': '0 0 8px #00FF88, 0 0 22px #00FF88, 0 0 36px rgba(0,255,136,0.35)',
|
||||
'--lt-glow-amber': '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)',
|
||||
'--lt-glow-amber': '0 0 6px #FFB300, 0 0 14px rgba(255,179,0,0.40)',
|
||||
'--lt-glow-amber-intense': '0 0 8px #FFB300, 0 0 20px #FFB300, 0 0 34px rgba(255,179,0,0.45)',
|
||||
'--lt-glow-red': '0 0 6px #FF2D55, 0 0 16px rgba(255,45,85,0.45)',
|
||||
'--lt-glow-red': '0 0 6px #FF2D55, 0 0 16px rgba(255,45,85,0.45)',
|
||||
// Glows — box
|
||||
'--lt-box-glow-orange': '0 0 18px rgba(255,107,0,0.22), 0 0 36px rgba(255,107,0,0.08)',
|
||||
'--lt-box-glow-cyan': '0 0 18px rgba(0,212,255,0.18), 0 0 36px rgba(0,212,255,0.06)',
|
||||
'--lt-box-glow-green': '0 0 18px rgba(0,255,136,0.18), 0 0 36px rgba(0,255,136,0.06)',
|
||||
'--lt-box-glow-red': '0 0 18px rgba(255,45,85,0.22), 0 0 36px rgba(255,45,85,0.08)',
|
||||
'--lt-box-glow-amber': '0 0 18px rgba(255,179,0,0.18), 0 0 36px rgba(255,179,0,0.06)',
|
||||
'--lt-box-glow-cyan': '0 0 18px rgba(0,212,255,0.18), 0 0 36px rgba(0,212,255,0.06)',
|
||||
'--lt-box-glow-green': '0 0 18px rgba(0,255,136,0.18), 0 0 36px rgba(0,255,136,0.06)',
|
||||
'--lt-box-glow-red': '0 0 18px rgba(255,45,85,0.22), 0 0 36px rgba(255,45,85,0.08)',
|
||||
'--lt-box-glow-amber': '0 0 18px rgba(255,179,0,0.18), 0 0 36px rgba(255,179,0,0.06)',
|
||||
// Fonts
|
||||
'--lt-font-mono': "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace",
|
||||
'--lt-font-mono': "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace",
|
||||
'--lt-font-display': "'JetBrains Mono', 'Fira Code', 'Courier New', monospace",
|
||||
'--lt-font-crt': "'VT323', 'Courier New', monospace",
|
||||
'--lt-font-crt': "'VT323', 'Courier New', monospace",
|
||||
} as any,
|
||||
});
|
||||
|
||||
@@ -97,7 +97,8 @@ globalStyle(`body.${lotusTerminalBodyClass}::before`, {
|
||||
content: "''",
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'repeating-linear-gradient(0deg, rgba(0,0,0,0.07) 0px, rgba(0,0,0,0.07) 1px, transparent 1px, transparent 3px)',
|
||||
background:
|
||||
'repeating-linear-gradient(0deg, rgba(0,0,0,0.07) 0px, rgba(0,0,0,0.07) 1px, transparent 1px, transparent 3px)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
});
|
||||
@@ -250,8 +251,7 @@ globalStyle(`body.${lotusTerminalBodyClass} hr`, {
|
||||
|
||||
// ── Input / textarea / contenteditable focus — orange glow ─────────────────
|
||||
globalStyle(
|
||||
`body.${lotusTerminalBodyClass} input:focus,` +
|
||||
`body.${lotusTerminalBodyClass} textarea:focus`,
|
||||
`body.${lotusTerminalBodyClass} input:focus,` + `body.${lotusTerminalBodyClass} textarea:focus`,
|
||||
{
|
||||
outline: 'none',
|
||||
borderColor: '#FF6B00',
|
||||
@@ -353,8 +353,6 @@ globalStyle(`body.${lotusTerminalBodyClass}`, {
|
||||
color: '#c4d9ee',
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ── Reaction chips (emoji reactions on messages) ────────────────────────────
|
||||
globalStyle(`body.${lotusTerminalBodyClass} button[data-reaction-key]`, {
|
||||
backgroundColor: 'rgba(0,212,255,0.06)',
|
||||
@@ -391,66 +389,66 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass}`, {
|
||||
backgroundImage: 'radial-gradient(circle, rgba(90,110,150,0.14) 1px, transparent 1px)',
|
||||
color: '#111827',
|
||||
vars: {
|
||||
'--lt-bg-primary': '#edf0f5',
|
||||
'--lt-bg-secondary': '#e2e7ef',
|
||||
'--lt-bg-tertiary': '#d4dae6',
|
||||
'--lt-bg-card': '#ffffff',
|
||||
'--lt-bg-terminal': '#f4f6fa',
|
||||
'--lt-accent-orange': '#c44e00',
|
||||
'--lt-accent-orange-bright':'#d45800',
|
||||
'--lt-accent-orange-dim': 'rgba(196,78,0,0.10)',
|
||||
'--lt-accent-orange-border':'rgba(196,78,0,0.28)',
|
||||
'--lt-accent-amber': '#8a5a00',
|
||||
'--lt-accent-amber-dim': 'rgba(138,90,0,0.10)',
|
||||
'--lt-accent-cyan': '#0062b8',
|
||||
'--lt-accent-cyan-bright': '#0070cc',
|
||||
'--lt-accent-cyan-dim': 'rgba(0,98,184,0.10)',
|
||||
'--lt-accent-cyan-border': 'rgba(0,98,184,0.22)',
|
||||
'--lt-accent-green': '#006d35',
|
||||
'--lt-bg-primary': '#edf0f5',
|
||||
'--lt-bg-secondary': '#e2e7ef',
|
||||
'--lt-bg-tertiary': '#d4dae6',
|
||||
'--lt-bg-card': '#ffffff',
|
||||
'--lt-bg-terminal': '#f4f6fa',
|
||||
'--lt-accent-orange': '#c44e00',
|
||||
'--lt-accent-orange-bright': '#d45800',
|
||||
'--lt-accent-orange-dim': 'rgba(196,78,0,0.10)',
|
||||
'--lt-accent-orange-border': 'rgba(196,78,0,0.28)',
|
||||
'--lt-accent-amber': '#8a5a00',
|
||||
'--lt-accent-amber-dim': 'rgba(138,90,0,0.10)',
|
||||
'--lt-accent-cyan': '#0062b8',
|
||||
'--lt-accent-cyan-bright': '#0070cc',
|
||||
'--lt-accent-cyan-dim': 'rgba(0,98,184,0.10)',
|
||||
'--lt-accent-cyan-border': 'rgba(0,98,184,0.22)',
|
||||
'--lt-accent-green': '#006d35',
|
||||
'--lt-accent-green-bright': '#007d3e',
|
||||
'--lt-accent-green-dim': 'rgba(0,109,53,0.10)',
|
||||
'--lt-accent-green-dim': 'rgba(0,109,53,0.10)',
|
||||
'--lt-accent-green-border': 'rgba(0,109,53,0.22)',
|
||||
'--lt-accent-red': '#b5001f',
|
||||
'--lt-accent-red-dim': 'rgba(181,0,31,0.12)',
|
||||
'--lt-accent-gold': '#8a5a00',
|
||||
'--lt-accent-gold-dim': 'rgba(138,90,0,0.10)',
|
||||
'--lt-accent-purple': '#6b2fb8',
|
||||
'--lt-accent-purple-dim': 'rgba(107,47,184,0.10)',
|
||||
'--lt-text-primary': '#111827',
|
||||
'--lt-text-secondary': '#2d3d56',
|
||||
'--lt-text-muted': '#5a6e8c',
|
||||
'--lt-text-dim': '#8a9ab8',
|
||||
'--lt-border-color': 'rgba(50,80,130,0.18)',
|
||||
'--lt-border-color-hi': '#0062b8',
|
||||
'--lt-border-color-dim': 'rgba(50,80,130,0.09)',
|
||||
'--lt-glow-orange': '0 0 0 1px rgba(196,78,0,0.25), 0 1px 6px rgba(196,78,0,0.18)',
|
||||
'--lt-accent-red': '#b5001f',
|
||||
'--lt-accent-red-dim': 'rgba(181,0,31,0.12)',
|
||||
'--lt-accent-gold': '#8a5a00',
|
||||
'--lt-accent-gold-dim': 'rgba(138,90,0,0.10)',
|
||||
'--lt-accent-purple': '#6b2fb8',
|
||||
'--lt-accent-purple-dim': 'rgba(107,47,184,0.10)',
|
||||
'--lt-text-primary': '#111827',
|
||||
'--lt-text-secondary': '#2d3d56',
|
||||
'--lt-text-muted': '#5a6e8c',
|
||||
'--lt-text-dim': '#8a9ab8',
|
||||
'--lt-border-color': 'rgba(50,80,130,0.18)',
|
||||
'--lt-border-color-hi': '#0062b8',
|
||||
'--lt-border-color-dim': 'rgba(50,80,130,0.09)',
|
||||
'--lt-glow-orange': '0 0 0 1px rgba(196,78,0,0.25), 0 1px 6px rgba(196,78,0,0.18)',
|
||||
'--lt-glow-orange-intense': '0 0 0 2px rgba(196,78,0,0.35), 0 2px 10px rgba(196,78,0,0.25)',
|
||||
'--lt-glow-cyan': '0 0 0 1px rgba(0,98,184,0.25), 0 1px 6px rgba(0,98,184,0.18)',
|
||||
'--lt-glow-cyan-intense': '0 0 0 2px rgba(0,98,184,0.35), 0 2px 10px rgba(0,98,184,0.25)',
|
||||
'--lt-glow-green': '0 0 0 1px rgba(0,109,53,0.25), 0 1px 6px rgba(0,109,53,0.18)',
|
||||
'--lt-glow-green-intense': '0 0 0 2px rgba(0,109,53,0.35), 0 2px 10px rgba(0,109,53,0.25)',
|
||||
'--lt-glow-amber': '0 0 0 1px rgba(138,90,0,0.25), 0 1px 6px rgba(138,90,0,0.18)',
|
||||
'--lt-glow-amber-intense': '0 0 0 2px rgba(138,90,0,0.35), 0 2px 10px rgba(138,90,0,0.25)',
|
||||
'--lt-glow-red': '0 0 0 1px rgba(181,0,31,0.25), 0 1px 6px rgba(181,0,31,0.18)',
|
||||
'--lt-box-glow-orange': '0 0 0 2px rgba(196,78,0,0.22), 0 2px 8px rgba(196,78,0,0.12)',
|
||||
'--lt-box-glow-cyan': '0 0 0 2px rgba(0,98,184,0.22), 0 2px 8px rgba(0,98,184,0.12)',
|
||||
'--lt-box-glow-green': '0 0 0 2px rgba(0,109,53,0.22), 0 2px 8px rgba(0,109,53,0.12)',
|
||||
'--lt-box-glow-red': '0 0 0 2px rgba(181,0,31,0.22), 0 2px 8px rgba(181,0,31,0.12)',
|
||||
'--lt-box-glow-amber': '0 0 0 2px rgba(138,90,0,0.22), 0 2px 8px rgba(138,90,0,0.12)',
|
||||
'--lt-glow-cyan': '0 0 0 1px rgba(0,98,184,0.25), 0 1px 6px rgba(0,98,184,0.18)',
|
||||
'--lt-glow-cyan-intense': '0 0 0 2px rgba(0,98,184,0.35), 0 2px 10px rgba(0,98,184,0.25)',
|
||||
'--lt-glow-green': '0 0 0 1px rgba(0,109,53,0.25), 0 1px 6px rgba(0,109,53,0.18)',
|
||||
'--lt-glow-green-intense': '0 0 0 2px rgba(0,109,53,0.35), 0 2px 10px rgba(0,109,53,0.25)',
|
||||
'--lt-glow-amber': '0 0 0 1px rgba(138,90,0,0.25), 0 1px 6px rgba(138,90,0,0.18)',
|
||||
'--lt-glow-amber-intense': '0 0 0 2px rgba(138,90,0,0.35), 0 2px 10px rgba(138,90,0,0.25)',
|
||||
'--lt-glow-red': '0 0 0 1px rgba(181,0,31,0.25), 0 1px 6px rgba(181,0,31,0.18)',
|
||||
'--lt-box-glow-orange': '0 0 0 2px rgba(196,78,0,0.22), 0 2px 8px rgba(196,78,0,0.12)',
|
||||
'--lt-box-glow-cyan': '0 0 0 2px rgba(0,98,184,0.22), 0 2px 8px rgba(0,98,184,0.12)',
|
||||
'--lt-box-glow-green': '0 0 0 2px rgba(0,109,53,0.22), 0 2px 8px rgba(0,109,53,0.12)',
|
||||
'--lt-box-glow-red': '0 0 0 2px rgba(181,0,31,0.22), 0 2px 8px rgba(181,0,31,0.12)',
|
||||
'--lt-box-glow-amber': '0 0 0 2px rgba(138,90,0,0.22), 0 2px 8px rgba(138,90,0,0.12)',
|
||||
} as any,
|
||||
});
|
||||
|
||||
// Scanlines + vignette: OFF in light mode (base.css:3676-3678)
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass}::before,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass}::after`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass}::after`,
|
||||
{ display: 'none' }
|
||||
);
|
||||
|
||||
// Caret
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} input,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} textarea`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} textarea`,
|
||||
{ caretColor: '#c44e00' }
|
||||
);
|
||||
|
||||
@@ -462,9 +460,12 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ::-webkit-s
|
||||
background: 'rgba(0,98,184,0.25)',
|
||||
borderRadius: '3px',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ::-webkit-scrollbar-thumb:hover`, {
|
||||
background: 'rgba(0,98,184,0.50)',
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} ::-webkit-scrollbar-thumb:hover`,
|
||||
{
|
||||
background: 'rgba(0,98,184,0.50)',
|
||||
}
|
||||
);
|
||||
|
||||
// Selection
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ::selection`, {
|
||||
@@ -500,12 +501,12 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} pre`, {
|
||||
// Inline semantics
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} strong,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} b`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} b`,
|
||||
{ color: '#c44e00' }
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} em,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} i`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} i`,
|
||||
{ color: '#0062b8' }
|
||||
);
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} mark`, {
|
||||
@@ -514,7 +515,7 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} mark`, {
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} del,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} s`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} s`,
|
||||
{ color: '#b5001f' }
|
||||
);
|
||||
|
||||
@@ -534,15 +535,18 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} hr`, {
|
||||
// Input focus
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} input:focus,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} textarea:focus`,
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} textarea:focus`,
|
||||
{
|
||||
borderColor: '#c44e00',
|
||||
boxShadow: '0 0 0 2px rgba(196,78,0,0.22), 0 1px 6px rgba(196,78,0,0.12)',
|
||||
}
|
||||
);
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [contenteditable="true"]:focus`, {
|
||||
boxShadow: '0 0 0 1px rgba(196,78,0,0.40), 0 1px 6px rgba(196,78,0,0.10)',
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [contenteditable="true"]:focus`,
|
||||
{
|
||||
boxShadow: '0 0 0 1px rgba(196,78,0,0.40), 0 1px 6px rgba(196,78,0,0.10)',
|
||||
}
|
||||
);
|
||||
|
||||
// Tables
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} table`, {
|
||||
@@ -610,33 +614,45 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data
|
||||
color: '#0062b8',
|
||||
transition: 'background 0.12s, border-color 0.12s, box-shadow 0.12s',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key]:hover`, {
|
||||
backgroundColor: 'rgba(0,98,184,0.12)',
|
||||
borderColor: 'rgba(0,98,184,0.42)',
|
||||
boxShadow: '0 0 7px rgba(0,98,184,0.16)',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]`, {
|
||||
backgroundColor: 'rgba(196,78,0,0.10)',
|
||||
border: '1px solid rgba(196,78,0,0.35)',
|
||||
color: '#c44e00',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]:hover`, {
|
||||
backgroundColor: 'rgba(196,78,0,0.18)',
|
||||
borderColor: 'rgba(196,78,0,0.55)',
|
||||
boxShadow: '0 0 7px rgba(196,78,0,0.18)',
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key]:hover`,
|
||||
{
|
||||
backgroundColor: 'rgba(0,98,184,0.12)',
|
||||
borderColor: 'rgba(0,98,184,0.42)',
|
||||
boxShadow: '0 0 7px rgba(0,98,184,0.16)',
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]`,
|
||||
{
|
||||
backgroundColor: 'rgba(196,78,0,0.10)',
|
||||
border: '1px solid rgba(196,78,0,0.35)',
|
||||
color: '#c44e00',
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]:hover`,
|
||||
{
|
||||
backgroundColor: 'rgba(196,78,0,0.18)',
|
||||
borderColor: 'rgba(196,78,0,0.55)',
|
||||
boxShadow: '0 0 7px rgba(196,78,0,0.18)',
|
||||
}
|
||||
);
|
||||
|
||||
// ── GIF picker (terminal mode) ───────────────────────────────────────────────
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] input,` +
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] form`, {
|
||||
background: '#030c14 !important' as any,
|
||||
color: '#e8edf5 !important' as any,
|
||||
fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any,
|
||||
border: '1px solid rgba(255,107,0,0.35) !important' as any,
|
||||
borderRadius: '4px !important' as any,
|
||||
fontSize: '12px !important' as any,
|
||||
boxShadow: 'none !important' as any,
|
||||
});
|
||||
globalStyle(
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] input,` +
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] form`,
|
||||
{
|
||||
background: '#030c14 !important' as any,
|
||||
color: '#e8edf5 !important' as any,
|
||||
fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any,
|
||||
border: '1px solid rgba(255,107,0,0.35) !important' as any,
|
||||
borderRadius: '4px !important' as any,
|
||||
fontSize: '12px !important' as any,
|
||||
boxShadow: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] input:focus`, {
|
||||
borderColor: 'rgba(255,107,0,0.70) !important' as any,
|
||||
boxShadow: '0 0 0 2px rgba(255,107,0,0.12) !important' as any,
|
||||
@@ -645,10 +661,13 @@ globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] input:focus`, {
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] input::placeholder`, {
|
||||
color: 'rgba(255,107,0,0.40) !important' as any,
|
||||
});
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] svg,` +
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`, {
|
||||
display: 'none !important' as any,
|
||||
});
|
||||
globalStyle(
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] svg,` +
|
||||
`body.${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`,
|
||||
{
|
||||
display: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(`body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar`, {
|
||||
width: '4px',
|
||||
});
|
||||
@@ -689,33 +708,50 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-url-p
|
||||
// ── GIF picker light TDS (dark-mode rules already exist via [data-gif-terminal]) ──
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] form`, {
|
||||
background: '#f4f6fa !important' as any,
|
||||
color: '#111827 !important' as any,
|
||||
border: '1px solid rgba(196,78,0,0.28) !important' as any,
|
||||
fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any,
|
||||
fontSize: '12px !important' as any,
|
||||
boxShadow: 'none !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input:focus`, {
|
||||
borderColor: 'rgba(196,78,0,0.60) !important' as any,
|
||||
boxShadow: '0 0 0 2px rgba(196,78,0,0.12) !important' as any,
|
||||
outline: 'none !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input::placeholder`, {
|
||||
color: 'rgba(196,78,0,0.45) !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] svg,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`, {
|
||||
display: 'none !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-track`, {
|
||||
background: '#e2e7ef',
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-thumb`, {
|
||||
background: 'rgba(196,78,0,0.35)',
|
||||
borderRadius: '2px',
|
||||
});
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] form`,
|
||||
{
|
||||
background: '#f4f6fa !important' as any,
|
||||
color: '#111827 !important' as any,
|
||||
border: '1px solid rgba(196,78,0,0.28) !important' as any,
|
||||
fontFamily: "'JetBrains Mono','Cascadia Code','Fira Code',monospace !important" as any,
|
||||
fontSize: '12px !important' as any,
|
||||
boxShadow: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input:focus`,
|
||||
{
|
||||
borderColor: 'rgba(196,78,0,0.60) !important' as any,
|
||||
boxShadow: '0 0 0 2px rgba(196,78,0,0.12) !important' as any,
|
||||
outline: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] input::placeholder`,
|
||||
{
|
||||
color: 'rgba(196,78,0,0.45) !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] svg,` +
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] button[type="reset"]`,
|
||||
{
|
||||
display: 'none !important' as any,
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-track`,
|
||||
{
|
||||
background: '#e2e7ef',
|
||||
}
|
||||
);
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-gif-terminal] ::-webkit-scrollbar-thumb`,
|
||||
{
|
||||
background: 'rgba(196,78,0,0.35)',
|
||||
borderRadius: '2px',
|
||||
}
|
||||
);
|
||||
|
||||
// ── Tooltip TDS ──────────────────────────────────────────────────────────────
|
||||
globalStyle(`body.${lotusTerminalBodyClass} ._6plmi2g > div`, {
|
||||
@@ -749,10 +785,13 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [role="swit
|
||||
background: 'rgba(0,98,184,0.10) !important' as any,
|
||||
borderColor: 'rgba(0,98,184,0.35) !important' as any,
|
||||
});
|
||||
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [role="switch"][aria-checked="true"]`, {
|
||||
background: 'rgba(196,78,0,0.18) !important' as any,
|
||||
borderColor: 'rgba(196,78,0,0.55) !important' as any,
|
||||
});
|
||||
globalStyle(
|
||||
`html[data-theme="light"] body.${lotusTerminalBodyClass} [role="switch"][aria-checked="true"]`,
|
||||
{
|
||||
background: 'rgba(196,78,0,0.18) !important' as any,
|
||||
borderColor: 'rgba(196,78,0,0.55) !important' as any,
|
||||
}
|
||||
);
|
||||
|
||||
// ── Spinner TDS ───────────────────────────────────────────────────────────────
|
||||
globalStyle(`body.${lotusTerminalBodyClass} ._31czpko`, {
|
||||
@@ -826,4 +865,3 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ._13tt0gb6:
|
||||
background: 'rgba(0,98,184,0.08) !important' as any,
|
||||
color: '#0062b8 !important' as any,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { MsgType } from 'matrix-js-sdk';
|
||||
|
||||
export const MATRIX_BLUR_HASH_PROPERTY_NAME = 'xyz.amorgan.blurhash';
|
||||
export const MATRIX_SPOILER_PROPERTY_NAME = 'page.codeberg.everypizza.msc4193.spoiler';
|
||||
export const MATRIX_SPOILER_REASON_PROPERTY_NAME = 'page.codeberg.everypizza.msc4193.spoiler.reason';
|
||||
export const MATRIX_SPOILER_REASON_PROPERTY_NAME =
|
||||
'page.codeberg.everypizza.msc4193.spoiler.reason';
|
||||
|
||||
export type IImageInfo = {
|
||||
w?: number;
|
||||
|
||||
@@ -10,7 +10,7 @@ function hashCode(str) {
|
||||
for (i = 0; i < str.length; i += 1) {
|
||||
chr = str.charCodeAt(i);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash = (hash << 5) - hash + chr;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
hash |= 0;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ async function deriveKeys(salt, iterations, password) {
|
||||
new TextEncoder().encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits'],
|
||||
['deriveBits']
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError(`subtleCrypto.importKey failed: ${e}`, cryptoFailMsg());
|
||||
@@ -53,40 +53,38 @@ async function deriveKeys(salt, iterations, password) {
|
||||
hash: 'SHA-512',
|
||||
},
|
||||
key,
|
||||
512,
|
||||
512
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError(`subtleCrypto.deriveBits failed: ${e}`, cryptoFailMsg());
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
console.log(`E2e import/export: deriveKeys took ${(now - start)}ms`);
|
||||
console.log(`E2e import/export: deriveKeys took ${now - start}ms`);
|
||||
|
||||
const aesKey = keybits.slice(0, 32);
|
||||
const hmacKey = keybits.slice(32);
|
||||
|
||||
const aesProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
aesKey,
|
||||
{ name: 'AES-CTR' },
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
).catch((e) => {
|
||||
throw friendlyError(`subtleCrypto.importKey failed for AES key: ${e}`, cryptoFailMsg());
|
||||
});
|
||||
const aesProm = subtleCrypto
|
||||
.importKey('raw', aesKey, { name: 'AES-CTR' }, false, ['encrypt', 'decrypt'])
|
||||
.catch((e) => {
|
||||
throw friendlyError(`subtleCrypto.importKey failed for AES key: ${e}`, cryptoFailMsg());
|
||||
});
|
||||
|
||||
const hmacProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
false,
|
||||
['sign', 'verify'],
|
||||
).catch((e) => {
|
||||
throw friendlyError(`subtleCrypto.importKey failed for HMAC key: ${e}`, cryptoFailMsg());
|
||||
});
|
||||
const hmacProm = subtleCrypto
|
||||
.importKey(
|
||||
'raw',
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
false,
|
||||
['sign', 'verify']
|
||||
)
|
||||
.catch((e) => {
|
||||
throw friendlyError(`subtleCrypto.importKey failed for HMAC key: ${e}`, cryptoFailMsg());
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-return-await
|
||||
return await Promise.all([aesProm, hmacProm]);
|
||||
@@ -177,7 +175,6 @@ function unpackMegolmKeyFile(data) {
|
||||
return decodeBase64(fileStr.slice(dataStart, dataEnd));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ascii-armour a megolm key file
|
||||
*
|
||||
@@ -189,20 +186,20 @@ function unpackMegolmKeyFile(data) {
|
||||
function packMegolmKeyFile(data) {
|
||||
// we split into lines before base64ing, because encodeBase64 doesn't deal
|
||||
// terribly well with large arrays.
|
||||
const LINE_LENGTH = ((72 * 4) / 3);
|
||||
const LINE_LENGTH = (72 * 4) / 3;
|
||||
const nLines = Math.ceil(data.length / LINE_LENGTH);
|
||||
const lines = new Array(nLines + 3);
|
||||
lines[0] = HEADER_LINE;
|
||||
let o = 0;
|
||||
let i;
|
||||
for (i = 1; i <= nLines; i += 1) {
|
||||
lines[i] = encodeBase64(data.subarray(o, o+LINE_LENGTH));
|
||||
lines[i] = encodeBase64(data.subarray(o, o + LINE_LENGTH));
|
||||
o += LINE_LENGTH;
|
||||
}
|
||||
lines[i] = TRAILER_LINE;
|
||||
i += 1;
|
||||
lines[i] = '';
|
||||
return (new TextEncoder().encode(lines.join('\n'))).buffer;
|
||||
return new TextEncoder().encode(lines.join('\n')).buffer;
|
||||
}
|
||||
|
||||
export async function decryptMegolmKeyFile(data, password) {
|
||||
@@ -225,7 +222,7 @@ export async function decryptMegolmKeyFile(data, password) {
|
||||
|
||||
const salt = body.subarray(1, 1 + 16);
|
||||
const iv = body.subarray(17, 17 + 16);
|
||||
const iterations = body[33] << 24 | body[34] << 16 | body[35] << 8 | body[36];
|
||||
const iterations = (body[33] << 24) | (body[34] << 16) | (body[35] << 8) | body[36];
|
||||
const ciphertext = body.subarray(37, 37 + ciphertextLength);
|
||||
const hmac = body.subarray(-32);
|
||||
|
||||
@@ -234,12 +231,7 @@ export async function decryptMegolmKeyFile(data, password) {
|
||||
|
||||
let isValid;
|
||||
try {
|
||||
isValid = await subtleCrypto.verify(
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
hmac,
|
||||
toVerify,
|
||||
);
|
||||
isValid = await subtleCrypto.verify({ name: 'HMAC' }, hmacKey, hmac, toVerify);
|
||||
} catch (e) {
|
||||
throw friendlyError(`subtleCrypto.verify failed: ${e}`, cryptoFailMsg());
|
||||
}
|
||||
@@ -256,7 +248,7 @@ export async function decryptMegolmKeyFile(data, password) {
|
||||
length: 64,
|
||||
},
|
||||
aesKey,
|
||||
ciphertext,
|
||||
ciphertext
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError(`subtleCrypto.decrypt failed: ${e}`, cryptoFailMsg());
|
||||
@@ -302,34 +294,33 @@ export async function encryptMegolmKeyFile(data, password, options) {
|
||||
length: 64,
|
||||
},
|
||||
aesKey,
|
||||
encodedData,
|
||||
encodedData
|
||||
);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.encrypt failed: ' + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
const cipherArray = new Uint8Array(ciphertext);
|
||||
const bodyLength = (1+salt.length+iv.length+4+cipherArray.length+32);
|
||||
const bodyLength = 1 + salt.length + iv.length + 4 + cipherArray.length + 32;
|
||||
const resultBuffer = new Uint8Array(bodyLength);
|
||||
let idx = 0;
|
||||
resultBuffer[idx++] = 1; // version
|
||||
resultBuffer.set(salt, idx); idx += salt.length;
|
||||
resultBuffer.set(iv, idx); idx += iv.length;
|
||||
resultBuffer.set(salt, idx);
|
||||
idx += salt.length;
|
||||
resultBuffer.set(iv, idx);
|
||||
idx += iv.length;
|
||||
resultBuffer[idx++] = kdfRounds >> 24;
|
||||
resultBuffer[idx++] = (kdfRounds >> 16) & 0xff;
|
||||
resultBuffer[idx++] = (kdfRounds >> 8) & 0xff;
|
||||
resultBuffer[idx++] = kdfRounds & 0xff;
|
||||
resultBuffer.set(cipherArray, idx); idx += cipherArray.length;
|
||||
resultBuffer.set(cipherArray, idx);
|
||||
idx += cipherArray.length;
|
||||
|
||||
const toSign = resultBuffer.subarray(0, idx);
|
||||
|
||||
let hmac;
|
||||
try {
|
||||
hmac = await subtleCrypto.sign(
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
toSign,
|
||||
);
|
||||
hmac = await subtleCrypto.sign({ name: 'HMAC' }, hmacKey, toSign);
|
||||
} catch (e) {
|
||||
throw friendlyError('subtleCrypto.sign failed: ' + e, cryptoFailMsg());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user