mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-05 23:10:28 +03:00
Move featured servers into the Featured section
This commit is contained in:
parent
3f5288fd09
commit
f0ace27d4a
7 changed files with 194 additions and 127 deletions
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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' }),
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue