Apply requested changes

This commit is contained in:
Ginger 2025-09-24 10:14:56 -04:00
parent 3cc7c085c2
commit 3eea7dd074
No known key found for this signature in database
3 changed files with 197 additions and 169 deletions

View file

@ -11,7 +11,7 @@ export const useExploreSelected = (): boolean => {
return !!match; return !!match;
}; };
export const useExploreFeaturedRooms = (): boolean => { export const useExploreFeaturedSelected = (): boolean => {
const match = useMatch({ const match = useMatch({
path: getExploreFeaturedPath(), path: getExploreFeaturedPath(),
caseSensitive: true, caseSensitive: true,

View file

@ -7,22 +7,22 @@ export type InCinnyExploreServersContent = {
servers?: string[]; servers?: string[];
}; };
export const useExploreServers = ( export const useExploreServers = (): [
exclude?: string[] string[],
): [string[], (server: string) => Promise<void>, (server: string) => Promise<void>] => { (server: string) => Promise<void>,
(server: string) => Promise<void>
] => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const accountData = useAccountData(AccountDataEvent.CinnyExplore); const accountData = useAccountData(AccountDataEvent.CinnyExplore);
const userAddedServers = useMemo( const userAddedServers = useMemo(
() => () => accountData?.getContent<InCinnyExploreServersContent>()?.servers ?? [],
accountData [accountData]
?.getContent<InCinnyExploreServersContent>()
?.servers?.filter((server) => !exclude?.includes(server)) ?? [],
[exclude, accountData]
); );
const addServer = useCallback( const addServer = useCallback(
async (server: string) => { async (server: string) => {
if (userAddedServers.indexOf(server) === -1) { if (userAddedServers.indexOf(server) === -1) {
// @ts-expect-error Custom account data event
await mx.setAccountData(AccountDataEvent.CinnyExplore, { await mx.setAccountData(AccountDataEvent.CinnyExplore, {
servers: [...userAddedServers, server], servers: [...userAddedServers, server],
}); });
@ -33,6 +33,7 @@ export const useExploreServers = (
const removeServer = useCallback( const removeServer = useCallback(
async (server: string) => { async (server: string) => {
// @ts-expect-error Custom account data event
await mx.setAccountData(AccountDataEvent.CinnyExplore, { await mx.setAccountData(AccountDataEvent.CinnyExplore, {
servers: userAddedServers.filter((addedServer) => server !== addedServer), servers: userAddedServers.filter((addedServer) => server !== addedServer),
}); });

View file

@ -32,7 +32,7 @@ import {
} from '../../../components/nav'; } from '../../../components/nav';
import { getExploreFeaturedPath, getExploreServerPath } from '../../pathUtils'; import { getExploreFeaturedPath, getExploreServerPath } from '../../pathUtils';
import { import {
useExploreFeaturedRooms, useExploreFeaturedSelected,
useExploreServer, useExploreServer,
} from '../../../hooks/router/useExploreSelected'; } from '../../../hooks/router/useExploreSelected';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
@ -44,21 +44,18 @@ import { stopPropagation } from '../../../utils/keyboard';
import { useExploreServers } from '../../../hooks/useExploreServers'; import { useExploreServers } from '../../../hooks/useExploreServers';
import { useAlive } from '../../../hooks/useAlive'; import { useAlive } from '../../../hooks/useAlive';
import { useClientConfig } from '../../../hooks/useClientConfig'; import { useClientConfig } from '../../../hooks/useClientConfig';
import { UseStateProvider } from '../../../components/UseStateProvider';
type AddExploreServerPromptProps = { type AddExploreServerPromptProps = {
onSubmit: (server: string, save: boolean) => Promise<void>; onSubmit: (server: string, save: boolean) => Promise<void>;
header: ReactNode; open: boolean;
children: ReactNode; requestClose: () => void;
selected?: boolean;
}; };
export function AddExploreServerPrompt({ export function AddExploreServerPrompt({
onSubmit, onSubmit,
header, open,
children, requestClose,
selected = false,
}: AddExploreServerPromptProps) { }: AddExploreServerPromptProps) {
const mx = useMatrixClient();
const [dialog, setDialog] = useState(false);
const alive = useAlive(); const alive = useAlive();
const serverInputRef = useRef<HTMLInputElement>(null); const serverInputRef = useRef<HTMLInputElement>(null);
@ -74,13 +71,12 @@ export function AddExploreServerPrompt({
const server = getInputServer(); const server = getInputServer();
if (!server) return; if (!server) return;
await mx.publicRooms({ server, limit: 1 });
await onSubmit(server, save); await onSubmit(server, save);
if (alive()) { if (alive()) {
setDialog(false); requestClose();
} }
}, },
[alive, onSubmit, mx] [onSubmit, alive, requestClose]
); );
const [viewState, handleView] = useAsyncCallback(() => submit(false)); const [viewState, handleView] = useAsyncCallback(() => submit(false));
@ -89,113 +85,91 @@ export function AddExploreServerPrompt({
viewState.status === AsyncStatus.Loading || saveViewState.status === AsyncStatus.Loading; viewState.status === AsyncStatus.Loading || saveViewState.status === AsyncStatus.Loading;
return ( return (
<> <Overlay open={open} backdrop={<OverlayBackdrop />}>
<Overlay open={dialog} backdrop={<OverlayBackdrop />}> <OverlayCenter>
<OverlayCenter> <FocusTrap
<FocusTrap focusTrapOptions={{
focusTrapOptions={{ initialFocus: false,
initialFocus: false, clickOutsideDeactivates: true,
clickOutsideDeactivates: true, onDeactivate: requestClose,
onDeactivate: () => setDialog(false), escapeDeactivates: stopPropagation,
escapeDeactivates: stopPropagation, }}
}} >
> <Dialog variant="Surface">
<Dialog variant="Surface"> <Header
<Header style={{
style={{ padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
padding: `0 ${config.space.S200} 0 ${config.space.S400}`, borderBottomWidth: config.borderWidth.B300,
borderBottomWidth: config.borderWidth.B300, }}
}} variant="Surface"
variant="Surface" size="500"
size="500" >
> <Box grow="Yes">
<Box grow="Yes">{header}</Box> <Text size="H4">Add Server</Text>
<IconButton size="300" onClick={() => setDialog(false)} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box 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 />
{viewState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to load public rooms. Please try again.
</Text>
)}
</Box>
<Box direction="Column" gap="200">
<Button
type="submit"
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">Save & View</Text>
</Button>
</Box>
</Box> </Box>
</Dialog> <IconButton size="300" onClick={requestClose} radii="300">
</FocusTrap> <Icon src={Icons.Cross} />
</OverlayCenter> </IconButton>
</Overlay> </Header>
<NavItem variant="Background" aria-selected={selected}> <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<NavButton onClick={() => setDialog(true)}> <Text priority="400">Add server name to explore public communities.</Text>
<NavItemContent>{children}</NavItemContent> <Box direction="Column" gap="100">
</NavButton> <Text size="L400">Server Name</Text>
</NavItem> <Input ref={serverInputRef} name="serverInput" variant="Background" required />
</> {viewState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to load public rooms. Please try again.
</Text>
)}
</Box>
<Box direction="Column" gap="200">
<Button
type="submit"
onClick={handleView}
variant="Primary"
fill="Soft"
before={
viewState.status === AsyncStatus.Loading && (
<Spinner fill="Soft" variant="Secondary" size="200" />
)
}
disabled={busy}
>
<Text size="B400">View</Text>
</Button>
<Button
type="submit"
onClick={handleSaveView}
variant="Primary"
fill="Solid"
before={
saveViewState.status === AsyncStatus.Loading && (
<Spinner fill="Solid" variant="Secondary" size="200" />
)
}
disabled={busy}
>
<Text size="B400">Save & View</Text>
</Button>
</Box>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
); );
} }
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;
action?: ExploreServerNavItemAction; after?: (hover: boolean) => ReactNode;
}; };
export function ExploreServerNavItem({ export function ExploreServerNavItem({ server, selected, icon, after }: ExploreServerNavItemProps) {
server,
selected,
icon,
action,
}: 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 [actionState, actionCallback] = useAsyncCallback(
useCallback(async () => {
await action?.onClick();
}, [action])
);
const actionInProgress =
actionState.status === AsyncStatus.Loading || actionState.status === AsyncStatus.Success;
return ( return (
<NavItem <NavItem
@ -219,7 +193,7 @@ export function ExploreServerNavItem({
</Box> </Box>
</NavItemContent> </NavItemContent>
</NavLink> </NavLink>
{action !== undefined && (hover || actionInProgress || action.alwaysVisible) && ( {/* {action !== undefined && (hover || actionInProgress || action.alwaysVisible) && (
<NavItemOptions> <NavItemOptions>
<IconButton <IconButton
onClick={actionCallback} onClick={actionCallback}
@ -236,7 +210,8 @@ export function ExploreServerNavItem({
)} )}
</IconButton> </IconButton>
</NavItemOptions> </NavItemOptions>
)} )} */}
{after && <NavItemOptions>{after(hover)}</NavItemOptions>}
</NavItem> </NavItem>
); );
} }
@ -253,10 +228,10 @@ export function Explore() {
clientConfig.featuredCommunities?.servers?.filter((server) => server !== userServer) ?? [], clientConfig.featuredCommunities?.servers?.filter((server) => server !== userServer) ?? [],
[clientConfig, userServer] [clientConfig, userServer]
); );
const [exploreServers, addServer, removeServer] = useExploreServers(featuredServers); const [exploreServers, addServer, removeServer] = useExploreServers();
const selectedServer = useExploreServer(); const selectedServer = useExploreServer();
const exploringFeaturedRooms = useExploreFeaturedRooms(); const exploringFeaturedRooms = useExploreFeaturedSelected();
const exploringUnlistedServer = useMemo( const exploringUnlistedServer = useMemo(
() => () =>
!( !(
@ -268,21 +243,25 @@ export function Explore() {
[exploreServers, selectedServer, userServer, featuredServers] [exploreServers, selectedServer, userServer, featuredServers]
); );
const addServerCallback = useCallback( const [addServerState, addServerCallback] = useAsyncCallback(
async (server: string, save: boolean) => { useCallback(
if (save && server !== userServer && !featuredServers.includes(server) && selectedServer) { async (server: string, save: boolean) => {
await addServer(server); if (save && server !== userServer && !featuredServers.includes(server) && selectedServer) {
} await addServer(server);
navigate(getExploreServerPath(server)); }
}, navigate(getExploreServerPath(server));
[addServer, navigate, userServer, featuredServers, selectedServer] },
[addServer, navigate, userServer, featuredServers, selectedServer]
)
); );
const removeServerCallback = useCallback( const [removeServerState, removeServerCallback] = useAsyncCallback(
async (server: string) => { useCallback(
await removeServer(server); async (server: string) => {
}, await removeServer(server);
[removeServer] },
[removeServer]
)
); );
return ( return (
@ -320,19 +299,7 @@ export function Explore() {
<ExploreServerNavItem <ExploreServerNavItem
server={userServer} server={userServer}
selected={userServer === selectedServer} selected={userServer === selectedServer}
icon={Icons.Home}
/>
)}
{exploringUnlistedServer && selectedServer !== undefined && (
<ExploreServerNavItem
server={selectedServer}
selected
icon={Icons.Server} icon={Icons.Server}
action={{
alwaysVisible: true,
icon: Icons.Plus,
onClick: () => addServerCallback(selectedServer, true),
}}
/> />
)} )}
</NavCategory> </NavCategory>
@ -350,34 +317,94 @@ export function Explore() {
icon={Icons.Server} icon={Icons.Server}
/> />
))} ))}
{exploreServers.map((server) => ( {exploreServers
.filter((server) => !featuredServers.includes(server))
.map((server) => (
<ExploreServerNavItem
key={server}
server={server}
selected={server === selectedServer}
icon={Icons.Server}
after={(hover) => {
const busy = removeServerState.status === AsyncStatus.Loading;
if (!(hover || busy)) {
return undefined;
}
return (
<IconButton
onClick={() => removeServerCallback(server)}
variant="Background"
fill="None"
size="300"
radii="300"
disabled={busy}
>
{busy ? (
<Spinner variant="Secondary" fill="Solid" size="200" />
) : (
<Icon size="50" src={Icons.Minus} />
)}
</IconButton>
);
}}
/>
))}
{exploringUnlistedServer && selectedServer !== undefined && (
<ExploreServerNavItem <ExploreServerNavItem
key={server} server={selectedServer}
server={server} selected
selected={server === selectedServer}
icon={Icons.Server} icon={Icons.Server}
action={{ after={() => {
alwaysVisible: false, const busy = addServerState.status === AsyncStatus.Loading;
icon: Icons.Minus,
onClick: () => removeServerCallback(server), return (
<IconButton
onClick={() => addServerCallback(selectedServer, true)}
variant="Background"
fill="None"
size="300"
radii="300"
disabled={busy}
>
{busy ? (
<Spinner variant="Secondary" fill="Solid" size="200" />
) : (
<Icon size="50" src={Icons.Bookmark} />
)}
</IconButton>
);
}} }}
/> />
))} )}
<AddExploreServerPrompt <UseStateProvider initial={false}>
onSubmit={addServerCallback} {(dialogOpen, setDialogOpen) => (
header={<Text size="H4">Add Server</Text>} <>
> <NavItem variant="Background">
<Box as="span" grow="Yes" alignItems="Center" gap="200"> <NavButton onClick={() => setDialogOpen(true)}>
<Avatar size="200" radii="400"> <NavItemContent>
<Icon src={Icons.Plus} size="100" /> <Box as="span" grow="Yes" alignItems="Center" gap="200">
</Avatar> <Avatar size="200" radii="400">
<Box as="span" grow="Yes"> <Icon src={Icons.Plus} size="100" />
<Text as="span" size="Inherit" truncate> </Avatar>
Add Server <Box as="span" grow="Yes">
</Text> <Text as="span" size="Inherit" truncate>
</Box> Add Server
</Box> </Text>
</AddExploreServerPrompt> </Box>
</Box>
</NavItemContent>
</NavButton>
</NavItem>
<AddExploreServerPrompt
onSubmit={addServerCallback}
open={dialogOpen}
requestClose={() => setDialogOpen(false)}
/>
</>
)}
</UseStateProvider>
</NavCategory> </NavCategory>
</Box> </Box>
</PageNavContent> </PageNavContent>