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
+120 -33
View File
@@ -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>} />
+6 -2
View File
@@ -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
+7 -1
View File
@@ -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">
+3 -1
View File
@@ -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 && (
<>
+2 -2
View File
@@ -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]);
};
+12 -4
View File
@@ -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>
</>
);
+2 -1
View File
@@ -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">
+9 -1
View File
@@ -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>
+7 -1
View File
@@ -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>
+16 -3
View File
@@ -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.
+6 -2
View File
@@ -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}>
+3 -1
View File
@@ -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) => (
+7 -1
View File
@@ -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>
+9 -3
View File
@@ -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
+7 -1
View File
@@ -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>
+10 -2
View File
@@ -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]