Move featured servers into the Featured section

This commit is contained in:
Ginger 2025-09-13 15:12:10 -04:00
parent 3f5288fd09
commit f0ace27d4a
No known key found for this signature in database
7 changed files with 194 additions and 127 deletions

View file

@ -4,5 +4,8 @@
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
} }
} }

View file

@ -5,6 +5,5 @@ export const InfoCard = style([
{ {
padding: config.space.S200, padding: config.space.S200,
borderRadius: config.radii.R300, borderRadius: config.radii.R300,
borderWidth: config.borderWidth.B300,
}, },
]); ]);

View file

@ -45,7 +45,7 @@ const setGridColumnCount = (grid: HTMLElement, count: GridColumnCount): void =>
grid.style.setProperty('grid-template-columns', `repeat(${count}, 1fr)`); grid.style.setProperty('grid-template-columns', `repeat(${count}, 1fr)`);
}; };
export function RoomCardGrid({ children }: { children: ReactNode }) { export function CardGrid({ children }: { children: ReactNode }) {
const gridRef = useRef<HTMLDivElement>(null); const gridRef = useRef<HTMLDivElement>(null);
useElementSizeObserver( useElementSizeObserver(
@ -60,17 +60,17 @@ export function RoomCardGrid({ children }: { children: ReactNode }) {
); );
} }
export const RoomCardBase = as<'div'>(({ className, ...props }, ref) => ( export const CardBase = as<'div'>(({ className, ...props }, ref) => (
<Box <Box
direction="Column" direction="Column"
gap="300" gap="300"
className={classNames(css.RoomCardBase, className)} className={classNames(css.CardBase, className)}
{...props} {...props}
ref={ref} ref={ref}
/> />
)); ));
export const RoomCardName = as<'h6'>(({ ...props }, ref) => ( export const CardName = as<'h6'>(({ ...props }, ref) => (
<Text as="h6" size="H6" truncate {...props} ref={ref} /> <Text as="h6" size="H6" truncate {...props} ref={ref} />
)); ));
@ -208,7 +208,7 @@ export const RoomCard = as<'div', RoomCardProps>(
const openTopic = () => setViewTopic(true); const openTopic = () => setViewTopic(true);
return ( return (
<RoomCardBase {...props} ref={ref}> <CardBase {...props} ref={ref}>
<Box gap="200" justifyContent="SpaceBetween"> <Box gap="200" justifyContent="SpaceBetween">
<Avatar size="500"> <Avatar size="500">
<RoomAvatar <RoomAvatar
@ -229,7 +229,7 @@ export const RoomCard = as<'div', RoomCardProps>(
)} )}
</Box> </Box>
<Box grow="Yes" direction="Column" gap="100"> <Box grow="Yes" direction="Column" gap="100">
<RoomCardName>{roomName}</RoomCardName> <CardName>{roomName}</CardName>
<RoomCardTopic onClick={openTopic} onKeyDown={onEnterOrSpace(openTopic)} tabIndex={0}> <RoomCardTopic onClick={openTopic} onKeyDown={onEnterOrSpace(openTopic)} tabIndex={0}>
{roomTopic} {roomTopic}
</RoomCardTopic> </RoomCardTopic>
@ -314,7 +314,7 @@ export const RoomCard = as<'div', RoomCardProps>(
</ErrorDialog> </ErrorDialog>
</Box> </Box>
)} )}
</RoomCardBase> </CardBase>
); );
} }
); );

View file

@ -8,7 +8,7 @@ export const CardGrid = style({
gap: config.space.S400, gap: config.space.S400,
}); });
export const RoomCardBase = style([ export const CardBase = style([
DefaultReset, DefaultReset,
ContainerColor({ variant: 'SurfaceVariant' }), ContainerColor({ variant: 'SurfaceVariant' }),
{ {

View file

@ -31,7 +31,6 @@ import {
NavLink, NavLink,
} from '../../../components/nav'; } from '../../../components/nav';
import { getExploreFeaturedPath, getExploreServerPath } from '../../pathUtils'; import { getExploreFeaturedPath, getExploreServerPath } from '../../pathUtils';
import { useClientConfig } from '../../../hooks/useClientConfig';
import { import {
useExploreFeaturedRooms, useExploreFeaturedRooms,
useExploreServer, useExploreServer,
@ -46,7 +45,7 @@ import { useExploreServers } from '../../../hooks/useExploreServers';
import { useAlive } from '../../../hooks/useAlive'; import { useAlive } from '../../../hooks/useAlive';
type AddExploreServerPromptProps = { type AddExploreServerPromptProps = {
onSubmit: (server: string) => Promise<void>; onSubmit: (server: string, save: boolean) => Promise<void>;
header: ReactNode; header: ReactNode;
children: ReactNode; children: ReactNode;
selected?: boolean; selected?: boolean;
@ -69,19 +68,25 @@ export function AddExploreServerPrompt({
return server || undefined; return server || undefined;
}; };
const [submitState, handleSubmit] = useAsyncCallback( const submit = useCallback(
useCallback(async () => { async (save: boolean) => {
const server = getInputServer(); const server = getInputServer();
if (!server) return; if (!server) return;
await mx.publicRooms({ server, limit: 1 }); await mx.publicRooms({ server, limit: 1 });
await onSubmit(server); await onSubmit(server, save);
if (alive()) { if (alive()) {
setDialog(false); setDialog(false);
} }
}, [alive, onSubmit, mx]) },
[alive, onSubmit, mx]
); );
const [viewState, handleView] = useAsyncCallback(() => submit(false));
const [saveViewState, handleSaveView] = useAsyncCallback(() => submit(true));
const busy =
viewState.status === AsyncStatus.Loading || saveViewState.status === AsyncStatus.Loading;
return ( return (
<> <>
<Overlay open={dialog} backdrop={<OverlayBackdrop />}> <Overlay open={dialog} backdrop={<OverlayBackdrop />}>
@ -108,21 +113,12 @@ export function AddExploreServerPrompt({
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
<Box <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
as="form"
onSubmit={(evt) => {
evt.preventDefault();
handleSubmit();
}}
style={{ padding: config.space.S400 }}
direction="Column"
gap="400"
>
<Text priority="400">Add server name to explore public communities.</Text> <Text priority="400">Add server name to explore public communities.</Text>
<Box direction="Column" gap="100"> <Box direction="Column" gap="100">
<Text size="L400">Server Name</Text> <Text size="L400">Server Name</Text>
<Input ref={serverInputRef} name="serverInput" variant="Background" required /> <Input ref={serverInputRef} name="serverInput" variant="Background" required />
{submitState.status === AsyncStatus.Error && ( {viewState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300"> <Text style={{ color: color.Critical.Main }} size="T300">
Failed to load public rooms. Please try again. Failed to load public rooms. Please try again.
</Text> </Text>
@ -131,18 +127,32 @@ export function AddExploreServerPrompt({
<Box direction="Column" gap="200"> <Box direction="Column" gap="200">
<Button <Button
type="submit" type="submit"
onClick={handleSubmit} onClick={handleView}
variant="Secondary" variant="Secondary"
fill="Soft" fill="Soft"
before={ before={
submitState.status === AsyncStatus.Loading && ( viewState.status === AsyncStatus.Loading && (
<Spinner fill="Solid" variant="Secondary" size="200" /> <Spinner fill="Solid" variant="Secondary" size="200" />
) )
} }
disabled={submitState.status === AsyncStatus.Loading} disabled={busy}
> >
<Text size="B400">View</Text> <Text size="B400">View</Text>
</Button> </Button>
<Button
type="submit"
onClick={handleSaveView}
variant="Primary"
fill="Soft"
before={
saveViewState.status === AsyncStatus.Loading && (
<Spinner fill="Solid" variant="Secondary" size="200" />
)
}
disabled={busy}
>
<Text size="B400">Save & View</Text>
</Button>
</Box> </Box>
</Box> </Box>
</Dialog> </Dialog>
@ -158,30 +168,33 @@ export function AddExploreServerPrompt({
); );
} }
type ExploreServerNavItemAction = {
onClick: () => Promise<void>;
icon: IconSrc;
alwaysVisible: boolean;
};
type ExploreServerNavItemProps = { type ExploreServerNavItemProps = {
server: string; server: string;
selected: boolean; selected: boolean;
icon: IconSrc; icon: IconSrc;
onRemove?: (() => Promise<void>) | null; action?: ExploreServerNavItemAction;
}; };
export function ExploreServerNavItem({ export function ExploreServerNavItem({
server, server,
selected, selected,
icon, icon,
onRemove = null, action,
}: ExploreServerNavItemProps) { }: ExploreServerNavItemProps) {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover }); const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [removeState, removeCallback] = useAsyncCallback( const [actionState, actionCallback] = useAsyncCallback(
useCallback(async () => { useCallback(async () => {
if (onRemove !== null) { await action?.onClick();
await onRemove(); }, [action])
}
}, [onRemove])
); );
const removeInProgress = const actionInProgress =
removeState.status === AsyncStatus.Loading || removeState.status === AsyncStatus.Success; actionState.status === AsyncStatus.Loading || actionState.status === AsyncStatus.Success;
return ( return (
<NavItem <NavItem
@ -205,20 +218,20 @@ export function ExploreServerNavItem({
</Box> </Box>
</NavItemContent> </NavItemContent>
</NavLink> </NavLink>
{onRemove !== null && (hover || removeInProgress) && ( {action !== undefined && (hover || actionInProgress || action.alwaysVisible) && (
<NavItemOptions> <NavItemOptions>
<IconButton <IconButton
onClick={removeCallback} onClick={actionCallback}
variant="Background" variant="Background"
fill="None" fill="None"
size="300" size="300"
radii="300" radii="300"
disabled={removeInProgress} disabled={actionInProgress}
> >
{removeInProgress ? ( {actionInProgress ? (
<Spinner variant="Secondary" fill="Solid" size="200" /> <Spinner variant="Secondary" fill="Solid" size="200" />
) : ( ) : (
<Icon size="50" src={Icons.Minus} /> <Icon size="50" src={action.icon} />
)} )}
</IconButton> </IconButton>
</NavItemOptions> </NavItemOptions>
@ -232,13 +245,7 @@ export function Explore() {
const navigate = useNavigate(); const navigate = useNavigate();
useNavToActivePathMapper('explore'); useNavToActivePathMapper('explore');
const userId = mx.getUserId(); const userId = mx.getUserId();
const clientConfig = useClientConfig();
const userServer = userId ? getMxIdServer(userId) : undefined; const userServer = userId ? getMxIdServer(userId) : undefined;
const featuredServers = useMemo(
() =>
clientConfig.featuredCommunities?.servers?.filter((server) => server !== userServer) ?? [],
[clientConfig, userServer]
);
const [exploreServers, addServer, removeServer] = useExploreServers(); const [exploreServers, addServer, removeServer] = useExploreServers();
const selectedServer = useExploreServer(); const selectedServer = useExploreServer();
@ -248,20 +255,19 @@ export function Explore() {
!( !(
selectedServer === undefined || selectedServer === undefined ||
selectedServer === userServer || selectedServer === userServer ||
featuredServers.includes(selectedServer) ||
exploreServers.includes(selectedServer) exploreServers.includes(selectedServer)
), ),
[exploreServers, featuredServers, selectedServer, userServer] [exploreServers, selectedServer, userServer]
); );
const addServerCallback = useCallback( const addServerCallback = useCallback(
async (server: string) => { async (server: string, save: boolean) => {
if (server !== userServer && selectedServer && !featuredServers.includes(selectedServer)) { if (save && server !== userServer && selectedServer) {
await addServer(server); await addServer(server);
} }
navigate(getExploreServerPath(server)); navigate(getExploreServerPath(server));
}, },
[addServer, navigate, userServer, featuredServers, selectedServer] [addServer, navigate, userServer, selectedServer]
); );
const removeServerCallback = useCallback( const removeServerCallback = useCallback(
@ -271,13 +277,6 @@ export function Explore() {
[removeServer] [removeServer]
); );
const exploreUnlistedServerCallback = useCallback(
async (server: string) => {
navigate(getExploreServerPath(server));
},
[navigate]
);
return ( return (
<PageNav> <PageNav>
<PageNavHeader> <PageNavHeader>
@ -293,29 +292,6 @@ export function Explore() {
<PageNavContent> <PageNavContent>
<Box direction="Column" gap="300"> <Box direction="Column" gap="300">
<NavCategory> <NavCategory>
<AddExploreServerPrompt
onSubmit={exploreUnlistedServerCallback}
header={<Text size="H4">View Server</Text>}
selected={exploringUnlistedServer}
>
<Box as="span" grow="Yes" alignItems="Center" gap="200">
<Avatar size="200" radii="400">
<Icon src={Icons.Link} size="100" />
</Avatar>
<Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate>
Explore with Address
</Text>
</Box>
</Box>
</AddExploreServerPrompt>
</NavCategory>
<NavCategory>
<NavCategoryHeader>
<Text size="O400" style={{ paddingLeft: config.space.S200 }}>
Featured
</Text>
</NavCategoryHeader>
<NavItem variant="Background" radii="400" aria-selected={exploringFeaturedRooms}> <NavItem variant="Background" radii="400" aria-selected={exploringFeaturedRooms}>
<NavLink to={getExploreFeaturedPath()}> <NavLink to={getExploreFeaturedPath()}>
<NavItemContent> <NavItemContent>
@ -325,7 +301,7 @@ export function Explore() {
</Avatar> </Avatar>
<Box as="span" grow="Yes"> <Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate> <Text as="span" size="Inherit" truncate>
Featured Rooms Featured
</Text> </Text>
</Box> </Box>
</Box> </Box>
@ -339,14 +315,18 @@ export function Explore() {
icon={Icons.Home} icon={Icons.Home}
/> />
)} )}
{featuredServers.map((server) => ( {exploringUnlistedServer && selectedServer !== undefined && (
<ExploreServerNavItem <ExploreServerNavItem
key={server} server={selectedServer}
server={server} selected
selected={server === selectedServer}
icon={Icons.Server} icon={Icons.Server}
action={{
alwaysVisible: true,
icon: Icons.Plus,
onClick: () => addServerCallback(selectedServer, true),
}}
/> />
))} )}
</NavCategory> </NavCategory>
<NavCategory> <NavCategory>
<NavCategoryHeader> <NavCategoryHeader>
@ -359,8 +339,12 @@ export function Explore() {
key={server} key={server}
server={server} server={server}
selected={server === selectedServer} selected={server === selectedServer}
onRemove={() => removeServerCallback(server)}
icon={Icons.Server} icon={Icons.Server}
action={{
alwaysVisible: false,
icon: Icons.Minus,
onClick: () => removeServerCallback(server),
}}
/> />
))} ))}
<AddExploreServerPrompt <AddExploreServerPrompt

View file

@ -1,8 +1,10 @@
import React from 'react'; import React, { useCallback } from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds'; import { Box, Button, color, Icon, IconButton, Icons, Scroll, Spinner, Text } from 'folds';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { useClientConfig } from '../../../hooks/useClientConfig'; import { useClientConfig } from '../../../hooks/useClientConfig';
import { RoomCard, RoomCardGrid } from '../../../components/room-card'; import { RoomCard, CardGrid, CardName, CardBase } from '../../../components/room-card';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { RoomSummaryLoader } from '../../../components/RoomSummaryLoader'; import { RoomSummaryLoader } from '../../../components/RoomSummaryLoader';
import { import {
@ -18,13 +20,69 @@ import * as css from './style.css';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { millify } from '../../../plugins/millify';
import { getExploreServerPath } from '../../pathUtils';
type ServerCardProps = {
serverName: string;
onExplore: () => unknown;
};
function ServerCard({ serverName, onExplore }: ServerCardProps) {
const mx = useMatrixClient();
const fetchPublicRooms = useCallback(
() => mx.publicRooms({ server: serverName }),
[mx, serverName]
);
const { data, isLoading, isError } = useQuery({
queryKey: [serverName, `publicRooms`],
queryFn: fetchPublicRooms,
});
const publicRoomCount = data?.total_room_count_estimate;
return (
<CardBase>
<CardName>{serverName}</CardName>
<Box gap="100" grow="Yes" style={isError ? { color: color.Critical.Main } : undefined}>
{isLoading ? (
<Spinner size="50" />
) : (
<>
<Icon size="50" src={isError ? Icons.Warning : Icons.Category} />
<Text size="T200">
{publicRoomCount === undefined
? 'Error loading rooms'
: `${millify(publicRoomCount)} Public Rooms`}
</Text>
</>
)}
</Box>
<Button onClick={onExplore} variant="Secondary" fill="Soft" size="300">
<Text size="B300" truncate>
Explore
</Text>
</Button>
</CardBase>
);
}
export function FeaturedRooms() { export function FeaturedRooms() {
const { featuredCommunities } = useClientConfig(); const { featuredCommunities } = useClientConfig();
const { rooms, spaces } = featuredCommunities ?? {}; const { rooms, spaces, servers } = featuredCommunities ?? {};
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const { navigateSpace, navigateRoom } = useRoomNavigate(); const { navigateSpace, navigateRoom } = useRoomNavigate();
const navigate = useNavigate();
const exploreServer = useCallback(
async (server: string) => {
navigate(getExploreServerPath(server));
},
[navigate]
);
return ( return (
<Page> <Page>
@ -49,15 +107,28 @@ export function FeaturedRooms() {
<PageHeroSection> <PageHeroSection>
<PageHero <PageHero
icon={<Icon size="600" src={Icons.Bulb} />} icon={<Icon size="600" src={Icons.Bulb} />}
title="Featured by Client" title="Featured"
subTitle="Find and explore public rooms and spaces featured by client provider." subTitle="Find and explore public communities featured by your client provider."
/> />
</PageHeroSection> </PageHeroSection>
<Box direction="Column" gap="700"> <Box direction="Column" gap="700">
{servers && servers.length > 0 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Servers</Text>
<CardGrid>
{servers.map((serverName) => (
<ServerCard
serverName={serverName}
onExplore={() => exploreServer(serverName)}
/>
))}
</CardGrid>
</Box>
)}
{spaces && spaces.length > 0 && ( {spaces && spaces.length > 0 && (
<Box direction="Column" gap="400"> <Box direction="Column" gap="400">
<Text size="H4">Featured Spaces</Text> <Text size="H4">Featured Spaces</Text>
<RoomCardGrid> <CardGrid>
{spaces.map((roomIdOrAlias) => ( {spaces.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}> <RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
{(roomSummary) => ( {(roomSummary) => (
@ -80,13 +151,13 @@ export function FeaturedRooms() {
)} )}
</RoomSummaryLoader> </RoomSummaryLoader>
))} ))}
</RoomCardGrid> </CardGrid>
</Box> </Box>
)} )}
{rooms && rooms.length > 0 && ( {rooms && rooms.length > 0 && (
<Box direction="Column" gap="400"> <Box direction="Column" gap="400">
<Text size="H4">Featured Rooms</Text> <Text size="H4">Featured Rooms</Text>
<RoomCardGrid> <CardGrid>
{rooms.map((roomIdOrAlias) => ( {rooms.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}> <RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
{(roomSummary) => ( {(roomSummary) => (
@ -109,7 +180,7 @@ export function FeaturedRooms() {
)} )}
</RoomSummaryLoader> </RoomSummaryLoader>
))} ))}
</RoomCardGrid> </CardGrid>
</Box> </Box>
)} )}
{((spaces && spaces.length === 0 && rooms && rooms.length === 0) || {((spaces && spaces.length === 0 && rooms && rooms.length === 0) ||
@ -123,7 +194,7 @@ export function FeaturedRooms() {
> >
<Icon size="400" src={Icons.Info} /> <Icon size="400" src={Icons.Info} />
<Text size="T300" align="Center"> <Text size="T300" align="Center">
No rooms or spaces featured by client provider. No rooms or spaces are featured.
</Text> </Text>
</Box> </Box>
)} )}

View file

@ -37,7 +37,7 @@ import { MatrixClient, Method, RoomType } from 'matrix-js-sdk';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page'; import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { RoomTopicViewer } from '../../../components/room-topic-viewer'; import { RoomTopicViewer } from '../../../components/room-topic-viewer';
import { RoomCard, RoomCardBase, RoomCardGrid } from '../../../components/room-card'; import { RoomCard, CardBase, CardGrid } from '../../../components/room-card';
import { ExploreServerPathSearchParams } from '../../paths'; import { ExploreServerPathSearchParams } from '../../paths';
import { getExploreServerPath, withSearchParam } from '../../pathUtils'; import { getExploreServerPath, withSearchParam } from '../../pathUtils';
import * as css from './style.css'; import * as css from './style.css';
@ -361,9 +361,9 @@ export function PublicRooms() {
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const roomTypeFilters = useRoomTypeFilters(); const roomTypeFilters = useRoomTypeFilters();
const [exploreServers, , removeServer] = useExploreServers(); const [exploreServers, addServer, removeServer] = useExploreServers();
const isUserAddedServer = server && exploreServers.includes(server); const isUserHomeserver = server && server === userServer;
const isUserHomeServer = server && server === userServer; const isBookmarkedServer = server && exploreServers.includes(server);
const currentLimit: number = useMemo(() => { const currentLimit: number = useMemo(() => {
const limitParam = serverSearchParams.limit; const limitParam = serverSearchParams.limit;
@ -480,15 +480,18 @@ export function PublicRooms() {
setMenuAnchor(evt.currentTarget.getBoundingClientRect()); setMenuAnchor(evt.currentTarget.getBoundingClientRect());
}; };
const [removeServerState, handleRemoveServer] = useAsyncCallback( const [menuActionState, handleMenuAction] = useAsyncCallback(
useCallback(async () => { useCallback(
if (!server) return; async (action: (server: string) => Promise<unknown>) => {
if (!server) return;
setMenuAnchor(undefined); setMenuAnchor(undefined);
await removeServer(server); await action(server);
}, [server, removeServer]) },
[server]
)
); );
const isRemoving = removeServerState.status === AsyncStatus.Loading; const menuActionBusy = menuActionState.status === AsyncStatus.Loading;
return ( return (
<Page> <Page>
@ -529,7 +532,9 @@ export function PublicRooms() {
)} )}
</Box> </Box>
<Box grow="Yes" basis="Yes" justifyContent="Center" alignItems="Center" gap="200"> <Box grow="Yes" basis="Yes" justifyContent="Center" alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={isUserHomeServer ? Icons.Home : Icons.Server} />} {screenSize !== ScreenSize.Mobile && (
<Icon size="400" src={isUserHomeserver ? Icons.Home : Icons.Server} />
)}
<Text size="H3" truncate> <Text size="H3" truncate>
{server} {server}
</Text> </Text>
@ -546,7 +551,7 @@ export function PublicRooms() {
} }
> >
{(triggerRef) => {(triggerRef) =>
isUserAddedServer && ( !isUserHomeserver && (
<IconButton <IconButton
onClick={handleOpenMenu} onClick={handleOpenMenu}
ref={triggerRef} ref={triggerRef}
@ -576,22 +581,27 @@ export function PublicRooms() {
<Menu style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu style={{ maxWidth: toRem(160), width: '100vw' }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem <MenuItem
onClick={handleRemoveServer} onClick={() =>
variant="Critical" handleMenuAction(isBookmarkedServer ? removeServer : addServer)
}
variant={isBookmarkedServer ? 'Critical' : 'Primary'}
fill="None" fill="None"
size="300" size="300"
after={ after={
isRemoving ? ( menuActionBusy ? (
<Spinner fill="Solid" variant="Secondary" size="200" /> <Spinner fill="Solid" variant="Secondary" size="200" />
) : ( ) : (
<Icon size="100" src={Icons.Delete} /> <Icon
size="100"
src={isBookmarkedServer ? Icons.Delete : Icons.Plus}
/>
) )
} }
radii="300" radii="300"
disabled={isRemoving} disabled={menuActionBusy}
> >
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Remove Server {isBookmarkedServer ? 'Remove Server' : 'Add Server'}
</Text> </Text>
</MenuItem> </MenuItem>
</Box> </Box>
@ -660,11 +670,11 @@ export function PublicRooms() {
</Box> </Box>
</Box> </Box>
{isLoading && ( {isLoading && (
<RoomCardGrid> <CardGrid>
{[...Array(currentLimit).keys()].map((item) => ( {[...Array(currentLimit).keys()].map((item) => (
<RoomCardBase key={item} style={{ minHeight: toRem(260) }} /> <CardBase key={item} style={{ minHeight: toRem(260) }} />
))} ))}
</RoomCardGrid> </CardGrid>
)} )}
{error && ( {error && (
<Box direction="Column" className={css.PublicRoomsError} gap="200"> <Box direction="Column" className={css.PublicRoomsError} gap="200">
@ -675,7 +685,7 @@ export function PublicRooms() {
{data && {data &&
(data.chunk.length > 0 ? ( (data.chunk.length > 0 ? (
<> <>
<RoomCardGrid> <CardGrid>
{data?.chunk.map((chunkRoom) => ( {data?.chunk.map((chunkRoom) => (
<RoomCard <RoomCard
key={chunkRoom.room_id} key={chunkRoom.room_id}
@ -700,7 +710,7 @@ export function PublicRooms() {
)} )}
/> />
))} ))}
</RoomCardGrid> </CardGrid>
{(data.prev_batch || data.next_batch) && ( {(data.prev_batch || data.next_batch) && (
<Box justifyContent="Center" gap="200"> <Box justifyContent="Center" gap="200">