chore: prettier format all files, brotli, Sentry release tagging, CI gates
CI / Build & Quality Checks (push) Failing after 5m12s

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lotus Bot
2026-05-21 20:49:33 -04:00
parent 04efb60fb2
commit fa50a45e84
105 changed files with 2749 additions and 1850 deletions
@@ -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">
+52 -28
View File
@@ -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>
+3 -1
View File
@@ -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}
>
+4 -1
View File
@@ -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">
+3 -1
View File
@@ -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
+4 -2
View File
@@ -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',
},
+1 -1
View File
@@ -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>
)}
+6 -1
View File
@@ -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>
)}
+68 -31
View File
@@ -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>
</>
+277 -147
View File
@@ -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>
);
}
+3 -4
View File
@@ -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]
);
+19 -7
View File
@@ -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>
)}
+7 -1
View File
@@ -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
+5 -1
View File
@@ -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>
+49 -21
View File
@@ -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>
)}