Revert "Add a page and config option for a room directory server"

This reverts commit 401d8ed930.

Revert "Minor styling changes"

This reverts commit 3de1ce8f2f.

Revert "Use consistent "bookmark" wording in code and UI"

This reverts commit c45cb62e2d.

Revert "Move featured servers into the Featured section"

This reverts commit 53612f4641.
This commit is contained in:
Ginger 2025-09-13 15:12:10 -04:00
parent 6a462d2801
commit be51dc5f12
No known key found for this signature in database
13 changed files with 279 additions and 377 deletions

View file

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

View file

@ -9,6 +9,7 @@
"xmr.se"
],
"allowCustomHomeservers": true,
"featuredCommunities": {
"openAsDefault": false,
"spaces": [
@ -27,16 +28,11 @@
"#PrivSec.dev:arcticfoxes.net",
"#disroot:aria-net.org"
],
"servers": [
"envs.net",
"matrix.org",
"monero.social",
"mozilla.org"
],
"directoryServer": "matrixrooms.info"
"servers": ["envs.net", "matrix.org", "monero.social", "mozilla.org"]
},
"hashRouter": {
"enabled": false,
"basename": "/"
}
}
}

View file

@ -5,5 +5,6 @@ export const InfoCard = style([
{
padding: config.space.S200,
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)`);
};
export function CardGrid({ children }: { children: ReactNode }) {
export function RoomCardGrid({ children }: { children: ReactNode }) {
const gridRef = useRef<HTMLDivElement>(null);
useElementSizeObserver(
@ -60,17 +60,17 @@ export function CardGrid({ children }: { children: ReactNode }) {
);
}
export const CardBase = as<'div'>(({ className, ...props }, ref) => (
export const RoomCardBase = as<'div'>(({ className, ...props }, ref) => (
<Box
direction="Column"
gap="300"
className={classNames(css.CardBase, className)}
className={classNames(css.RoomCardBase, className)}
{...props}
ref={ref}
/>
));
export const CardName = as<'h6'>(({ ...props }, ref) => (
export const RoomCardName = as<'h6'>(({ ...props }, ref) => (
<Text as="h6" size="H6" truncate {...props} ref={ref} />
));
@ -208,7 +208,7 @@ export const RoomCard = as<'div', RoomCardProps>(
const openTopic = () => setViewTopic(true);
return (
<CardBase {...props} ref={ref}>
<RoomCardBase {...props} ref={ref}>
<Box gap="200" justifyContent="SpaceBetween">
<Avatar size="500">
<RoomAvatar
@ -229,7 +229,7 @@ export const RoomCard = as<'div', RoomCardProps>(
)}
</Box>
<Box grow="Yes" direction="Column" gap="100">
<CardName>{roomName}</CardName>
<RoomCardName>{roomName}</RoomCardName>
<RoomCardTopic onClick={openTopic} onKeyDown={onEnterOrSpace(openTopic)} tabIndex={0}>
{roomTopic}
</RoomCardTopic>
@ -314,7 +314,7 @@ export const RoomCard = as<'div', RoomCardProps>(
</ErrorDialog>
</Box>
)}
</CardBase>
</RoomCardBase>
);
}
);

View file

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

View file

@ -1,6 +1,5 @@
import { useMatch, useParams } from 'react-router-dom';
import { getExploreFeaturedPath, getExplorePath } from '../../pages/pathUtils';
import { useClientConfig } from '../useClientConfig';
export const useExploreSelected = (): boolean => {
const match = useMatch({
@ -27,8 +26,3 @@ export const useExploreServer = (): string | undefined => {
return server;
};
export const useDirectoryServer = (): string | undefined => {
const { featuredCommunities } = useClientConfig();
return featuredCommunities?.directoryServer;
};

View file

@ -1,43 +0,0 @@
import { useCallback, useMemo } from 'react';
import { AccountDataEvent } from '../../types/matrix/accountData';
import { useAccountData } from './useAccountData';
import { useMatrixClient } from './useMatrixClient';
export type InCinnyBookmarkedServersContent = {
servers?: string[];
};
export const useBookmarkedServers = (): [
string[],
(server: string) => Promise<void>,
(server: string) => Promise<void>
] => {
const mx = useMatrixClient();
const accountData = useAccountData(AccountDataEvent.CinnyBookmarkedServers);
const bookmarkedServers = useMemo(
() => accountData?.getContent<InCinnyBookmarkedServersContent>()?.servers ?? [],
[accountData]
);
const addServerBookmark = useCallback(
async (server: string) => {
if (bookmarkedServers.indexOf(server) === -1) {
await mx.setAccountData(AccountDataEvent.CinnyBookmarkedServers, {
servers: [...bookmarkedServers, server],
});
}
},
[mx, bookmarkedServers]
);
const removeServerBookmark = useCallback(
async (server: string) => {
await mx.setAccountData(AccountDataEvent.CinnyBookmarkedServers, {
servers: bookmarkedServers.filter((addedServer) => server !== addedServer),
});
},
[mx, bookmarkedServers]
);
return [bookmarkedServers, addServerBookmark, removeServerBookmark];
};

View file

@ -15,7 +15,6 @@ export type ClientConfig = {
spaces?: string[];
rooms?: string[];
servers?: string[];
directoryServer?: string;
};
hashRouter?: HashRouterConfig;

View file

@ -0,0 +1,43 @@
import { useCallback, useMemo } from 'react';
import { AccountDataEvent } from '../../types/matrix/accountData';
import { useAccountData } from './useAccountData';
import { useMatrixClient } from './useMatrixClient';
export type InCinnyExploreServersContent = {
servers?: string[];
};
export const useExploreServers = (): [
string[],
(server: string) => Promise<void>,
(server: string) => Promise<void>
] => {
const mx = useMatrixClient();
const accountData = useAccountData(AccountDataEvent.CinnyExplore);
const userAddedServers = useMemo(
() => accountData?.getContent<InCinnyExploreServersContent>()?.servers ?? [],
[accountData]
);
const addServer = useCallback(
async (server: string) => {
if (userAddedServers.indexOf(server) === -1) {
await mx.setAccountData(AccountDataEvent.CinnyExplore, {
servers: [...userAddedServers, server],
});
}
},
[mx, userAddedServers]
);
const removeServer = useCallback(
async (server: string) => {
await mx.setAccountData(AccountDataEvent.CinnyExplore, {
servers: userAddedServers.filter((addedServer) => server !== addedServer),
});
},
[mx, userAddedServers]
);
return [userAddedServers, addServer, removeServer];
};

View file

@ -31,8 +31,8 @@ import {
NavLink,
} from '../../../components/nav';
import { getExploreFeaturedPath, getExploreServerPath } from '../../pathUtils';
import { useClientConfig } from '../../../hooks/useClientConfig';
import {
useDirectoryServer,
useExploreFeaturedRooms,
useExploreServer,
} from '../../../hooks/router/useExploreSelected';
@ -42,21 +42,21 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
import { stopPropagation } from '../../../utils/keyboard';
import { useBookmarkedServers } from '../../../hooks/useBookmarkedServers';
import { useExploreServers } from '../../../hooks/useExploreServers';
import { useAlive } from '../../../hooks/useAlive';
type ExploreServerPromptProps = {
onSubmit: (server: string, save: boolean) => Promise<void>;
type AddExploreServerPromptProps = {
onSubmit: (server: string) => Promise<void>;
header: ReactNode;
children: ReactNode;
selected?: boolean;
};
export function ExploreServerPrompt({
export function AddExploreServerPrompt({
onSubmit,
header,
children,
selected = false,
}: ExploreServerPromptProps) {
}: AddExploreServerPromptProps) {
const mx = useMatrixClient();
const [dialog, setDialog] = useState(false);
const alive = useAlive();
@ -69,27 +69,19 @@ export function ExploreServerPrompt({
return server || undefined;
};
const handleSubmit = useCallback(
async (saveBookmark: boolean) => {
const [submitState, handleSubmit] = useAsyncCallback(
useCallback(async () => {
const server = getInputServer();
if (!server) return;
await mx.publicRooms({ server, limit: 1 });
await onSubmit(server, saveBookmark);
await onSubmit(server);
if (alive()) {
setDialog(false);
}
},
[alive, onSubmit, mx]
}, [alive, onSubmit, mx])
);
const [viewState, handleView] = useAsyncCallback(() => handleSubmit(false));
const [saveViewState, handleSaveView] = useAsyncCallback(() => handleSubmit(true));
const busy =
viewState.status === AsyncStatus.Loading || saveViewState.status === AsyncStatus.Loading;
const failed =
viewState.status === AsyncStatus.Error || saveViewState.status === AsyncStatus.Error;
return (
<>
<Overlay open={dialog} backdrop={<OverlayBackdrop />}>
@ -116,46 +108,41 @@ export function ExploreServerPrompt({
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box as="form" style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box
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>
<Box direction="Column" gap="100">
<Text size="L400">Server Name</Text>
<Input ref={serverInputRef} name="serverInput" variant="Background" required />
{failed && (
{submitState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to load public rooms. Please try again.
</Text>
)}
<Box direction="Column" gap="200">
<Button
type="button"
onClick={handleView}
variant="Secondary"
fill="Soft"
before={
viewState.status === AsyncStatus.Loading && (
<Spinner fill="Solid" variant="Secondary" size="200" />
)
}
disabled={busy}
>
<Text size="B400">View</Text>
</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">Bookmark & View</Text>
</Button>
</Box>
</Box>
<Box direction="Column" gap="200">
<Button
type="submit"
onClick={handleSubmit}
variant="Secondary"
fill="Soft"
before={
submitState.status === AsyncStatus.Loading && (
<Spinner fill="Solid" variant="Secondary" size="200" />
)
}
disabled={submitState.status === AsyncStatus.Loading}
>
<Text size="B400">View</Text>
</Button>
</Box>
</Box>
</Dialog>
@ -171,34 +158,30 @@ export function ExploreServerPrompt({
);
}
type ExploreServerNavItemAction = {
onClick: () => Promise<void>;
icon: IconSrc;
filled?: boolean;
alwaysVisible: boolean;
};
type ExploreServerNavItemProps = {
server: string;
selected: boolean;
icon: IconSrc;
action?: ExploreServerNavItemAction;
onRemove?: (() => Promise<void>) | null;
};
export function ExploreServerNavItem({
server,
selected,
icon,
action,
onRemove = null,
}: ExploreServerNavItemProps) {
const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover });
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [actionState, actionCallback] = useAsyncCallback(
const [removeState, removeCallback] = useAsyncCallback(
useCallback(async () => {
await action?.onClick();
}, [action])
if (onRemove !== null) {
await onRemove();
}
}, [onRemove])
);
const actionInProgress =
actionState.status === AsyncStatus.Loading || actionState.status === AsyncStatus.Success;
const removeInProgress =
removeState.status === AsyncStatus.Loading || removeState.status === AsyncStatus.Success;
return (
<NavItem
@ -222,21 +205,20 @@ export function ExploreServerNavItem({
</Box>
</NavItemContent>
</NavLink>
{action !== undefined && (hover || actionInProgress || action.alwaysVisible) && (
{onRemove !== null && (hover || removeInProgress) && (
<NavItemOptions>
<IconButton
onClick={actionCallback}
variant={selected ? 'Background' : 'Surface'}
onClick={removeCallback}
variant="Background"
fill="None"
outlined={action.alwaysVisible}
size="300"
radii="300"
disabled={actionInProgress}
disabled={removeInProgress}
>
{actionInProgress ? (
<Spinner variant="Secondary" fill="Solid" size="50" />
{removeInProgress ? (
<Spinner variant="Secondary" fill="Solid" size="200" />
) : (
<Icon size="50" src={action.icon} filled={action.filled} />
<Icon size="50" src={Icons.Minus} />
)}
</IconButton>
</NavItemOptions>
@ -250,39 +232,50 @@ export function Explore() {
const navigate = useNavigate();
useNavToActivePathMapper('explore');
const userId = mx.getUserId();
const clientConfig = useClientConfig();
const userServer = userId ? getMxIdServer(userId) : undefined;
const directoryServer = useDirectoryServer();
const [bookmarkedServers, addServerBookmark, removeServerBookmark] = useBookmarkedServers();
const selectedServer = useExploreServer();
const featuredServers = useMemo(
() =>
clientConfig.featuredCommunities?.servers?.filter((server) => server !== userServer) ?? [],
[clientConfig, userServer]
);
const [exploreServers, addServer, removeServer] = useExploreServers();
const selectedServer = useExploreServer();
const exploringFeaturedRooms = useExploreFeaturedRooms();
const exploringDirectory = directoryServer !== undefined && selectedServer === directoryServer;
const exploringUnlistedServer = useMemo(
() =>
!(
selectedServer === undefined ||
selectedServer === userServer ||
exploringDirectory ||
bookmarkedServers.includes(selectedServer)
featuredServers.includes(selectedServer) ||
exploreServers.includes(selectedServer)
),
[bookmarkedServers, selectedServer, userServer, exploringDirectory]
[exploreServers, featuredServers, selectedServer, userServer]
);
const viewServerCallback = useCallback(
async (server: string, saveBookmark: boolean) => {
if (saveBookmark && server !== userServer && server !== directoryServer && selectedServer) {
await addServerBookmark(server);
const addServerCallback = useCallback(
async (server: string) => {
if (server !== userServer && selectedServer && !featuredServers.includes(selectedServer)) {
await addServer(server);
}
navigate(getExploreServerPath(server));
},
[addServerBookmark, navigate, userServer, directoryServer, selectedServer]
[addServer, navigate, userServer, featuredServers, selectedServer]
);
const removeServerBookmarkCallback = useCallback(
const removeServerCallback = useCallback(
async (server: string) => {
await removeServerBookmark(server);
await removeServer(server);
},
[removeServerBookmark]
[removeServer]
);
const exploreUnlistedServerCallback = useCallback(
async (server: string) => {
navigate(getExploreServerPath(server));
},
[navigate]
);
return (
@ -300,6 +293,29 @@ export function Explore() {
<PageNavContent>
<Box direction="Column" gap="300">
<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}>
<NavLink to={getExploreFeaturedPath()}>
<NavItemContent>
@ -309,31 +325,13 @@ export function Explore() {
</Avatar>
<Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate>
Featured
Featured Rooms
</Text>
</Box>
</Box>
</NavItemContent>
</NavLink>
</NavItem>
{directoryServer && (
<NavItem variant="Background" radii="400" aria-selected={exploringDirectory}>
<NavLink to={getExploreServerPath(directoryServer)}>
<NavItemContent>
<Box as="span" grow="Yes" alignItems="Center" gap="200">
<Avatar size="200" radii="400">
<Icon src={Icons.Search} size="100" filled={exploringDirectory} />
</Avatar>
<Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate>
Public Room Directory
</Text>
</Box>
</Box>
</NavItemContent>
</NavLink>
</NavItem>
)}
{userServer && (
<ExploreServerNavItem
server={userServer}
@ -341,40 +339,32 @@ export function Explore() {
icon={Icons.Home}
/>
)}
{exploringUnlistedServer && selectedServer !== undefined && (
<ExploreServerNavItem
server={selectedServer}
selected
icon={Icons.Eye}
action={{
alwaysVisible: true,
icon: Icons.Bookmark,
onClick: () => viewServerCallback(selectedServer, true),
}}
/>
)}
</NavCategory>
<NavCategory>
<NavCategoryHeader>
<Text size="O400" style={{ paddingLeft: config.space.S200 }}>
Bookmarks
</Text>
</NavCategoryHeader>
{bookmarkedServers.map((server) => (
{featuredServers.map((server) => (
<ExploreServerNavItem
key={server}
server={server}
selected={server === selectedServer}
icon={Icons.Server}
action={{
alwaysVisible: false,
icon: Icons.Minus,
onClick: () => removeServerBookmarkCallback(server),
}}
/>
))}
<ExploreServerPrompt
onSubmit={viewServerCallback}
</NavCategory>
<NavCategory>
<NavCategoryHeader>
<Text size="O400" style={{ paddingLeft: config.space.S200 }}>
Servers
</Text>
</NavCategoryHeader>
{exploreServers.map((server) => (
<ExploreServerNavItem
key={server}
server={server}
selected={server === selectedServer}
onRemove={() => removeServerCallback(server)}
icon={Icons.Server}
/>
))}
<AddExploreServerPrompt
onSubmit={addServerCallback}
header={<Text size="H4">Add Server</Text>}
>
<Box as="span" grow="Yes" alignItems="Center" gap="200">
@ -387,7 +377,7 @@ export function Explore() {
</Text>
</Box>
</Box>
</ExploreServerPrompt>
</AddExploreServerPrompt>
</NavCategory>
</Box>
</PageNavContent>

View file

@ -1,10 +1,8 @@
import React, { useCallback } from 'react';
import { Box, Button, color, Icon, IconButton, Icons, Scroll, Spinner, Text } from 'folds';
import React from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { useAtomValue } from 'jotai';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { useClientConfig } from '../../../hooks/useClientConfig';
import { RoomCard, CardGrid, CardName, CardBase } from '../../../components/room-card';
import { RoomCard, RoomCardGrid } from '../../../components/room-card';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { RoomSummaryLoader } from '../../../components/RoomSummaryLoader';
import {
@ -20,69 +18,13 @@ import * as css from './style.css';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
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" size="300">
<Text size="B300" truncate>
Explore Rooms
</Text>
</Button>
</CardBase>
);
}
export function FeaturedRooms() {
const { featuredCommunities } = useClientConfig();
const { rooms, spaces, servers } = featuredCommunities ?? {};
const { rooms, spaces } = featuredCommunities ?? {};
const allRooms = useAtomValue(allRoomsAtom);
const screenSize = useScreenSizeContext();
const { navigateSpace, navigateRoom } = useRoomNavigate();
const navigate = useNavigate();
const exploreServer = useCallback(
async (server: string) => {
navigate(getExploreServerPath(server));
},
[navigate]
);
return (
<Page>
@ -107,28 +49,15 @@ export function FeaturedRooms() {
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Bulb} />}
title="Featured"
subTitle="Find and explore public communities featured by your client provider."
title="Featured by Client"
subTitle="Find and explore public rooms and spaces featured by client provider."
/>
</PageHeroSection>
<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 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Spaces</Text>
<CardGrid>
<RoomCardGrid>
{spaces.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
{(roomSummary) => (
@ -151,13 +80,13 @@ export function FeaturedRooms() {
)}
</RoomSummaryLoader>
))}
</CardGrid>
</RoomCardGrid>
</Box>
)}
{rooms && rooms.length > 0 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Rooms</Text>
<CardGrid>
<RoomCardGrid>
{rooms.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
{(roomSummary) => (
@ -180,7 +109,7 @@ export function FeaturedRooms() {
)}
</RoomSummaryLoader>
))}
</CardGrid>
</RoomCardGrid>
</Box>
)}
{((spaces && spaces.length === 0 && rooms && rooms.length === 0) ||
@ -194,7 +123,7 @@ export function FeaturedRooms() {
>
<Icon size="400" src={Icons.Info} />
<Text size="T300" align="Center">
No rooms or spaces are featured.
No rooms or spaces featured by client provider.
</Text>
</Box>
)}

View file

@ -9,13 +9,11 @@ import React, {
useState,
} from 'react';
import {
Badge,
Box,
Button,
Chip,
Icon,
IconButton,
IconSrc,
Icons,
Input,
Line,
@ -39,7 +37,7 @@ import { MatrixClient, Method, RoomType } from 'matrix-js-sdk';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { RoomTopicViewer } from '../../../components/room-topic-viewer';
import { RoomCard, CardBase, CardGrid } from '../../../components/room-card';
import { RoomCard, RoomCardBase, RoomCardGrid } from '../../../components/room-card';
import { ExploreServerPathSearchParams } from '../../paths';
import { getExploreServerPath, withSearchParam } from '../../pathUtils';
import * as css from './style.css';
@ -50,8 +48,7 @@ import { stopPropagation } from '../../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useBookmarkedServers } from '../../../hooks/useBookmarkedServers';
import { useDirectoryServer } from '../../../hooks/router/useExploreSelected';
import { useExploreServers } from '../../../hooks/useExploreServers';
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
useMemo(
@ -352,10 +349,10 @@ export function PublicRooms() {
const mx = useMatrixClient();
const userId = mx.getUserId();
const userServer = userId && getMxIdServer(userId);
const directoryServer = useDirectoryServer();
const allRooms = useAtomValue(allRoomsAtom);
const { navigateSpace, navigateRoom } = useRoomNavigate();
const screenSize = useScreenSizeContext();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [searchParams] = useSearchParams();
const serverSearchParams = useServerSearchParams(searchParams);
@ -364,18 +361,9 @@ export function PublicRooms() {
const searchInputRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate();
const roomTypeFilters = useRoomTypeFilters();
const [bookmarkedServers, addServerBookmark, removeServerBookmark] = useBookmarkedServers();
const isUserHomeserver = server !== undefined && server === userServer;
const isBookmarkedServer = server !== undefined && bookmarkedServers.includes(server);
const isDirectoryServer = server !== undefined && server === directoryServer;
let headerIcon: IconSrc;
if (isUserHomeserver) {
headerIcon = Icons.Home;
} else if (isDirectoryServer) {
headerIcon = Icons.Search;
} else {
headerIcon = Icons.Server;
}
const [exploreServers, , removeServer] = useExploreServers();
const isUserAddedServer = server && exploreServers.includes(server);
const isUserHomeServer = server && server === userServer;
const currentLimit: number = useMemo(() => {
const limitParam = serverSearchParams.limit;
@ -488,17 +476,19 @@ export function PublicRooms() {
explore({ instance: instanceId, since: undefined });
};
const [bookmarkActionState, handleBookmarkAction] = useAsyncCallback(
useCallback(
async (action: (server: string) => Promise<unknown>) => {
if (!server) return;
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
await action(server);
},
[server]
)
const [removeServerState, handleRemoveServer] = useAsyncCallback(
useCallback(async () => {
if (!server) return;
setMenuAnchor(undefined);
await removeServer(server);
}, [server, removeServer])
);
const bookmarkActionLoading = bookmarkActionState.status === AsyncStatus.Loading;
const isRemoving = removeServerState.status === AsyncStatus.Loading;
return (
<Page>
@ -539,70 +529,76 @@ export function PublicRooms() {
)}
</Box>
<Box grow="Yes" basis="Yes" justifyContent="Center" alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={headerIcon} />}
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={isUserHomeServer ? Icons.Home : Icons.Server} />}
<Text size="H3" truncate>
{isDirectoryServer ? 'Public Room Directory' : server}
{server}
</Text>
</Box>
<Box shrink="No" grow="Yes" basis="No" justifyContent="End">
{isDirectoryServer ? (
<TooltipProvider
position="Bottom"
align="End"
offset={4}
tooltip={
<Tooltip>
<Text>provided by {directoryServer}</Text>
</Tooltip>
}
>
{(triggerRef) => (
<Chip
as="a"
href={`https://${directoryServer}`}
target="_blank"
rel="noreferrer noopener"
variant="Secondary"
radii="Pill"
fill="Soft"
ref={screenSize === ScreenSize.Mobile ? triggerRef : undefined}
style={{ maxWidth: '100%' }}
before={<Icon src={Icons.External} size="50" />}
<TooltipProvider
position="Bottom"
align="End"
offset={4}
tooltip={
<Tooltip>
<Text>More Options</Text>
</Tooltip>
}
>
{(triggerRef) =>
isUserAddedServer && (
<IconButton
onClick={handleOpenMenu}
ref={triggerRef}
aria-pressed={!!menuAnchor}
>
<Text size="T200" truncate>
provided by {directoryServer}
</Text>
</Chip>
)}
</TooltipProvider>
) : (
<TooltipProvider
position="Bottom"
align="End"
offset={4}
tooltip={
<Tooltip>
<Text>{isBookmarkedServer ? 'Remove Bookmark' : 'Add Bookmark'}</Text>
</Tooltip>
}
>
{(triggerRef) =>
!isUserHomeserver && (
<IconButton
onClick={() =>
handleBookmarkAction(
isBookmarkedServer ? removeServerBookmark : addServerBookmark
)
}
ref={triggerRef}
disabled={bookmarkActionLoading}
>
<Icon size="400" src={Icons.Bookmark} filled={isBookmarkedServer} />
</IconButton>
)
}
</TooltipProvider>
)}
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
</IconButton>
)
}
</TooltipProvider>
<PopOut
anchor={menuAnchor}
position="Bottom"
align="End"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
returnFocusOnDeactivate: false,
onDeactivate: () => setMenuAnchor(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu style={{ maxWidth: toRem(160), width: '100vw' }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem
onClick={handleRemoveServer}
variant="Critical"
fill="None"
size="300"
after={
isRemoving ? (
<Spinner fill="Solid" variant="Secondary" size="200" />
) : (
<Icon size="100" src={Icons.Delete} />
)
}
radii="300"
disabled={isRemoving}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Remove Server
</Text>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
/>
</Box>
</>
)}
@ -664,11 +660,11 @@ export function PublicRooms() {
</Box>
</Box>
{isLoading && (
<CardGrid>
<RoomCardGrid>
{[...Array(currentLimit).keys()].map((item) => (
<CardBase key={item} style={{ minHeight: toRem(260) }} />
<RoomCardBase key={item} style={{ minHeight: toRem(260) }} />
))}
</CardGrid>
</RoomCardGrid>
)}
{error && (
<Box direction="Column" className={css.PublicRoomsError} gap="200">
@ -679,7 +675,7 @@ export function PublicRooms() {
{data &&
(data.chunk.length > 0 ? (
<>
<CardGrid>
<RoomCardGrid>
{data?.chunk.map((chunkRoom) => (
<RoomCard
key={chunkRoom.room_id}
@ -704,7 +700,7 @@ export function PublicRooms() {
)}
/>
))}
</CardGrid>
</RoomCardGrid>
{(data.prev_batch || data.next_batch) && (
<Box justifyContent="Center" gap="200">

View file

@ -4,7 +4,7 @@ export enum AccountDataEvent {
IgnoredUserList = 'm.ignored_user_list',
CinnySpaces = 'in.cinny.spaces',
CinnyBookmarkedServers = 'in.cinny.bookmarked_servers',
CinnyExplore = 'in.cinny.explore',
ElementRecentEmoji = 'io.element.recent_emoji',