Use consistent "bookmark" wording in code and UI

This commit is contained in:
Ginger 2025-09-05 12:14:02 -04:00
parent 53612f4641
commit c45cb62e2d
No known key found for this signature in database
6 changed files with 129 additions and 173 deletions

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 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

@ -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 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

@ -41,21 +41,21 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page'; import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { useExploreServers } from '../../../hooks/useExploreServers'; import { useBookmarkedServers } from '../../../hooks/useBookmarkedServers';
import { useAlive } from '../../../hooks/useAlive'; import { useAlive } from '../../../hooks/useAlive';
type AddExploreServerPromptProps = { type ExploreServerPromptProps = {
onSubmit: (server: string, save: boolean) => Promise<void>; onSubmit: (server: string, save: boolean) => Promise<void>;
header: ReactNode; header: ReactNode;
children: ReactNode; children: ReactNode;
selected?: boolean; selected?: boolean;
}; };
export function AddExploreServerPrompt({ export function ExploreServerPrompt({
onSubmit, onSubmit,
header, header,
children, children,
selected = false, selected = false,
}: AddExploreServerPromptProps) { }: ExploreServerPromptProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [dialog, setDialog] = useState(false); const [dialog, setDialog] = useState(false);
const alive = useAlive(); const alive = useAlive();
@ -68,13 +68,13 @@ export function AddExploreServerPrompt({
return server || undefined; return server || undefined;
}; };
const submit = useCallback( const handleSubmit = useCallback(
async (save: boolean) => { async (saveBookmark: 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, save); await onSubmit(server, saveBookmark);
if (alive()) { if (alive()) {
setDialog(false); setDialog(false);
} }
@ -82,10 +82,12 @@ export function AddExploreServerPrompt({
[alive, onSubmit, mx] [alive, onSubmit, mx]
); );
const [viewState, handleView] = useAsyncCallback(() => submit(false)); const [viewState, handleView] = useAsyncCallback(() => handleSubmit(false));
const [saveViewState, handleSaveView] = useAsyncCallback(() => submit(true)); const [saveViewState, handleSaveView] = useAsyncCallback(() => handleSubmit(true));
const busy = const busy =
viewState.status === AsyncStatus.Loading || saveViewState.status === AsyncStatus.Loading; viewState.status === AsyncStatus.Loading || saveViewState.status === AsyncStatus.Loading;
const failed =
viewState.status === AsyncStatus.Error || saveViewState.status === AsyncStatus.Error;
return ( return (
<> <>
@ -113,20 +115,19 @@ export function AddExploreServerPrompt({
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400"> <Box as="form" 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 />
{viewState.status === AsyncStatus.Error && ( {failed && (
<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>
)} )}
</Box>
<Box direction="Column" gap="200"> <Box direction="Column" gap="200">
<Button <Button
type="submit" type="button"
onClick={handleView} onClick={handleView}
variant="Secondary" variant="Secondary"
fill="Soft" fill="Soft"
@ -151,10 +152,11 @@ export function AddExploreServerPrompt({
} }
disabled={busy} disabled={busy}
> >
<Text size="B400">Save & View</Text> <Text size="B400">Bookmark & View</Text>
</Button> </Button>
</Box> </Box>
</Box> </Box>
</Box>
</Dialog> </Dialog>
</FocusTrap> </FocusTrap>
</OverlayCenter> </OverlayCenter>
@ -171,6 +173,7 @@ export function AddExploreServerPrompt({
type ExploreServerNavItemAction = { type ExploreServerNavItemAction = {
onClick: () => Promise<void>; onClick: () => Promise<void>;
icon: IconSrc; icon: IconSrc;
filled?: boolean;
alwaysVisible: boolean; alwaysVisible: boolean;
}; };
type ExploreServerNavItemProps = { type ExploreServerNavItemProps = {
@ -222,16 +225,17 @@ export function ExploreServerNavItem({
<NavItemOptions> <NavItemOptions>
<IconButton <IconButton
onClick={actionCallback} onClick={actionCallback}
variant="Background" variant={selected ? 'Background' : 'Surface'}
fill="None" fill="None"
outlined={action.alwaysVisible}
size="300" size="300"
radii="300" radii="300"
disabled={actionInProgress} disabled={actionInProgress}
> >
{actionInProgress ? ( {actionInProgress ? (
<Spinner variant="Secondary" fill="Solid" size="200" /> <Spinner variant="Secondary" fill="Solid" size="50" />
) : ( ) : (
<Icon size="50" src={action.icon} /> <Icon size="50" src={action.icon} filled={action.filled} />
)} )}
</IconButton> </IconButton>
</NavItemOptions> </NavItemOptions>
@ -246,7 +250,7 @@ export function Explore() {
useNavToActivePathMapper('explore'); useNavToActivePathMapper('explore');
const userId = mx.getUserId(); const userId = mx.getUserId();
const userServer = userId ? getMxIdServer(userId) : undefined; const userServer = userId ? getMxIdServer(userId) : undefined;
const [exploreServers, addServer, removeServer] = useExploreServers(); const [bookmarkedServers, addServerBookmark, removeServerBookmark] = useBookmarkedServers();
const selectedServer = useExploreServer(); const selectedServer = useExploreServer();
const exploringFeaturedRooms = useExploreFeaturedRooms(); const exploringFeaturedRooms = useExploreFeaturedRooms();
@ -255,26 +259,26 @@ export function Explore() {
!( !(
selectedServer === undefined || selectedServer === undefined ||
selectedServer === userServer || selectedServer === userServer ||
exploreServers.includes(selectedServer) bookmarkedServers.includes(selectedServer)
), ),
[exploreServers, selectedServer, userServer] [bookmarkedServers, selectedServer, userServer]
); );
const addServerCallback = useCallback( const viewServerCallback = useCallback(
async (server: string, save: boolean) => { async (server: string, saveBookmark: boolean) => {
if (save && server !== userServer && selectedServer) { if (saveBookmark && server !== userServer && selectedServer) {
await addServer(server); await addServerBookmark(server);
} }
navigate(getExploreServerPath(server)); navigate(getExploreServerPath(server));
}, },
[addServer, navigate, userServer, selectedServer] [addServerBookmark, navigate, userServer, selectedServer]
); );
const removeServerCallback = useCallback( const removeServerBookmarkCallback = useCallback(
async (server: string) => { async (server: string) => {
await removeServer(server); await removeServerBookmark(server);
}, },
[removeServer] [removeServerBookmark]
); );
return ( return (
@ -319,11 +323,11 @@ export function Explore() {
<ExploreServerNavItem <ExploreServerNavItem
server={selectedServer} server={selectedServer}
selected selected
icon={Icons.Server} icon={Icons.Eye}
action={{ action={{
alwaysVisible: true, alwaysVisible: true,
icon: Icons.Plus, icon: Icons.Bookmark,
onClick: () => addServerCallback(selectedServer, true), onClick: () => viewServerCallback(selectedServer, true),
}} }}
/> />
)} )}
@ -331,10 +335,10 @@ export function Explore() {
<NavCategory> <NavCategory>
<NavCategoryHeader> <NavCategoryHeader>
<Text size="O400" style={{ paddingLeft: config.space.S200 }}> <Text size="O400" style={{ paddingLeft: config.space.S200 }}>
Servers Bookmarks
</Text> </Text>
</NavCategoryHeader> </NavCategoryHeader>
{exploreServers.map((server) => ( {bookmarkedServers.map((server) => (
<ExploreServerNavItem <ExploreServerNavItem
key={server} key={server}
server={server} server={server}
@ -342,13 +346,14 @@ export function Explore() {
icon={Icons.Server} icon={Icons.Server}
action={{ action={{
alwaysVisible: false, alwaysVisible: false,
icon: Icons.Minus, icon: Icons.Bookmark,
onClick: () => removeServerCallback(server), filled: true,
onClick: () => removeServerBookmarkCallback(server),
}} }}
/> />
))} ))}
<AddExploreServerPrompt <ExploreServerPrompt
onSubmit={addServerCallback} onSubmit={viewServerCallback}
header={<Text size="H4">Add Server</Text>} header={<Text size="H4">Add Server</Text>}
> >
<Box as="span" grow="Yes" alignItems="Center" gap="200"> <Box as="span" grow="Yes" alignItems="Center" gap="200">
@ -361,7 +366,7 @@ export function Explore() {
</Text> </Text>
</Box> </Box>
</Box> </Box>
</AddExploreServerPrompt> </ExploreServerPrompt>
</NavCategory> </NavCategory>
</Box> </Box>
</PageNavContent> </PageNavContent>

View file

@ -62,7 +62,7 @@ function ServerCard({ serverName, onExplore }: ServerCardProps) {
</Box> </Box>
<Button onClick={onExplore} variant="Secondary" fill="Soft" size="300"> <Button onClick={onExplore} variant="Secondary" fill="Soft" size="300">
<Text size="B300" truncate> <Text size="B300" truncate>
Explore Explore Rooms
</Text> </Text>
</Button> </Button>
</CardBase> </CardBase>

View file

@ -48,7 +48,7 @@ import { stopPropagation } from '../../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler'; import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useExploreServers } from '../../../hooks/useExploreServers'; import { useBookmarkedServers } from '../../../hooks/useBookmarkedServers';
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams => const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
useMemo( useMemo(
@ -352,7 +352,6 @@ export function PublicRooms() {
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
const { navigateSpace, navigateRoom } = useRoomNavigate(); const { navigateSpace, navigateRoom } = useRoomNavigate();
const screenSize = useScreenSizeContext(); const screenSize = useScreenSizeContext();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const serverSearchParams = useServerSearchParams(searchParams); const serverSearchParams = useServerSearchParams(searchParams);
@ -361,9 +360,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, addServer, removeServer] = useExploreServers(); const [bookmarkedServers, addServerBookmark, removeServerBookmark] = useBookmarkedServers();
const isUserHomeserver = server && server === userServer; const isUserHomeserver = server !== undefined && server === userServer;
const isBookmarkedServer = server && exploreServers.includes(server); const isBookmarkedServer = server !== undefined && bookmarkedServers.includes(server);
const currentLimit: number = useMemo(() => { const currentLimit: number = useMemo(() => {
const limitParam = serverSearchParams.limit; const limitParam = serverSearchParams.limit;
@ -476,22 +475,17 @@ export function PublicRooms() {
explore({ instance: instanceId, since: undefined }); explore({ instance: instanceId, since: undefined });
}; };
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => { const [bookmarkActionState, handleBookmarkAction] = useAsyncCallback(
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
const [menuActionState, handleMenuAction] = useAsyncCallback(
useCallback( useCallback(
async (action: (server: string) => Promise<unknown>) => { async (action: (server: string) => Promise<unknown>) => {
if (!server) return; if (!server) return;
setMenuAnchor(undefined);
await action(server); await action(server);
}, },
[server] [server]
) )
); );
const menuActionBusy = menuActionState.status === AsyncStatus.Loading; const bookmarkActionLoading = bookmarkActionState.status === AsyncStatus.Loading;
return ( return (
<Page> <Page>
@ -546,69 +540,26 @@ export function PublicRooms() {
offset={4} offset={4}
tooltip={ tooltip={
<Tooltip> <Tooltip>
<Text>More Options</Text> <Text>{isBookmarkedServer ? 'Remove Bookmark' : 'Add Bookmark'}</Text>
</Tooltip> </Tooltip>
} }
> >
{(triggerRef) => {(triggerRef) =>
!isUserHomeserver && ( !isUserHomeserver && (
<IconButton <IconButton
onClick={handleOpenMenu} onClick={() =>
handleBookmarkAction(
isBookmarkedServer ? removeServerBookmark : addServerBookmark
)
}
ref={triggerRef} ref={triggerRef}
aria-pressed={!!menuAnchor} disabled={bookmarkActionLoading}
> >
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} /> <Icon size="400" src={Icons.Bookmark} filled={isBookmarkedServer} />
</IconButton> </IconButton>
) )
} }
</TooltipProvider> </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={() =>
handleMenuAction(isBookmarkedServer ? removeServer : addServer)
}
variant={isBookmarkedServer ? 'Critical' : 'Primary'}
fill="None"
size="300"
after={
menuActionBusy ? (
<Spinner fill="Solid" variant="Secondary" size="200" />
) : (
<Icon
size="100"
src={isBookmarkedServer ? Icons.Delete : Icons.Plus}
/>
)
}
radii="300"
disabled={menuActionBusy}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
{isBookmarkedServer ? 'Remove Server' : 'Add Server'}
</Text>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
/>
</Box> </Box>
</> </>
)} )}

View file

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