mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-09-13 14:22:25 +03:00
Merge 8e81f8725e
into 31c6d13fdf
This commit is contained in:
commit
c359e08e04
6 changed files with 376 additions and 122 deletions
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -1,5 +1,8 @@
|
||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const useExploreSelected = (): boolean => {
|
||||||
return !!match;
|
return !!match;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useExploreFeaturedSelected = (): boolean => {
|
export const useExploreFeaturedRooms = (): boolean => {
|
||||||
const match = useMatch({
|
const match = useMatch({
|
||||||
path: getExploreFeaturedPath(),
|
path: getExploreFeaturedPath(),
|
||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
|
|
44
src/app/hooks/useExploreServers.ts
Normal file
44
src/app/hooks/useExploreServers.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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 = (
|
||||||
|
exclude?: string[]
|
||||||
|
): [string[], (server: string) => Promise<void>, (server: string) => Promise<void>] => {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const accountData = useAccountData(AccountDataEvent.CinnyExplore);
|
||||||
|
const userAddedServers = useMemo(
|
||||||
|
() =>
|
||||||
|
accountData
|
||||||
|
?.getContent<InCinnyExploreServersContent>()
|
||||||
|
?.servers?.filter((server) => !exclude?.includes(server)) ?? [],
|
||||||
|
[exclude, 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];
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FormEventHandler, useCallback, useRef, useState } from 'react';
|
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import {
|
import {
|
||||||
|
@ -9,26 +9,30 @@ import {
|
||||||
Header,
|
Header,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
IconSrc,
|
||||||
Icons,
|
Icons,
|
||||||
Input,
|
Input,
|
||||||
Overlay,
|
Overlay,
|
||||||
OverlayBackdrop,
|
OverlayBackdrop,
|
||||||
OverlayCenter,
|
OverlayCenter,
|
||||||
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
color,
|
color,
|
||||||
config,
|
config,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
|
import { useFocusWithin, useHover } from 'react-aria';
|
||||||
import {
|
import {
|
||||||
|
NavButton,
|
||||||
NavCategory,
|
NavCategory,
|
||||||
NavCategoryHeader,
|
NavCategoryHeader,
|
||||||
NavItem,
|
NavItem,
|
||||||
NavItemContent,
|
NavItemContent,
|
||||||
|
NavItemOptions,
|
||||||
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 {
|
||||||
useExploreFeaturedSelected,
|
useExploreFeaturedRooms,
|
||||||
useExploreServer,
|
useExploreServer,
|
||||||
} from '../../../hooks/router/useExploreSelected';
|
} from '../../../hooks/router/useExploreSelected';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
@ -37,17 +41,27 @@ 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 { useAlive } from '../../../hooks/useAlive';
|
||||||
|
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||||
|
|
||||||
export function AddServer() {
|
type AddExploreServerPromptProps = {
|
||||||
|
onSubmit: (server: string, save: boolean) => Promise<void>;
|
||||||
|
header: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
selected?: boolean;
|
||||||
|
};
|
||||||
|
export function AddExploreServerPrompt({
|
||||||
|
onSubmit,
|
||||||
|
header,
|
||||||
|
children,
|
||||||
|
selected = false,
|
||||||
|
}: AddExploreServerPromptProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const navigate = useNavigate();
|
|
||||||
const [dialog, setDialog] = useState(false);
|
const [dialog, setDialog] = useState(false);
|
||||||
|
const alive = useAlive();
|
||||||
const serverInputRef = useRef<HTMLInputElement>(null);
|
const serverInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [exploreState] = useAsyncCallback(
|
|
||||||
useCallback((server: string) => mx.publicRooms({ server, limit: 1 }), [mx])
|
|
||||||
);
|
|
||||||
|
|
||||||
const getInputServer = (): string | undefined => {
|
const getInputServer = (): string | undefined => {
|
||||||
const serverInput = serverInputRef.current;
|
const serverInput = serverInputRef.current;
|
||||||
if (!serverInput) return undefined;
|
if (!serverInput) return undefined;
|
||||||
|
@ -55,22 +69,24 @@ export function AddServer() {
|
||||||
return server || undefined;
|
return server || undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
const submit = useCallback(
|
||||||
evt.preventDefault();
|
async (save: boolean) => {
|
||||||
const server = getInputServer();
|
const server = getInputServer();
|
||||||
if (!server) return;
|
if (!server) return;
|
||||||
// explore(server);
|
|
||||||
|
|
||||||
navigate(getExploreServerPath(server));
|
await mx.publicRooms({ server, limit: 1 });
|
||||||
setDialog(false);
|
await onSubmit(server, save);
|
||||||
};
|
if (alive()) {
|
||||||
|
setDialog(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[alive, onSubmit, mx]
|
||||||
|
);
|
||||||
|
|
||||||
const handleView = () => {
|
const [viewState, handleView] = useAsyncCallback(() => submit(false));
|
||||||
const server = getInputServer();
|
const [saveViewState, handleSaveView] = useAsyncCallback(() => submit(true));
|
||||||
if (!server) return;
|
const busy =
|
||||||
navigate(getExploreServerPath(server));
|
viewState.status === AsyncStatus.Loading || saveViewState.status === AsyncStatus.Loading;
|
||||||
setDialog(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -93,79 +109,181 @@ export function AddServer() {
|
||||||
variant="Surface"
|
variant="Surface"
|
||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">{header}</Box>
|
||||||
<Text size="H4">Add Server</Text>
|
|
||||||
</Box>
|
|
||||||
<IconButton size="300" onClick={() => setDialog(false)} radii="300">
|
<IconButton size="300" onClick={() => setDialog(false)} radii="300">
|
||||||
<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={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 />
|
||||||
{exploreState.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>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
{/* <Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
onClick={handleView}
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
before={
|
before={
|
||||||
exploreState.status === AsyncStatus.Loading ? (
|
viewState.status === AsyncStatus.Loading && (
|
||||||
<Spinner fill="Solid" variant="Secondary" size="200" />
|
<Spinner fill="Solid" variant="Secondary" size="200" />
|
||||||
) : undefined
|
)
|
||||||
}
|
}
|
||||||
aria-disabled={exploreState.status === AsyncStatus.Loading}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
<Text size="B400">Save</Text>
|
|
||||||
</Button> */}
|
|
||||||
|
|
||||||
<Button type="submit" onClick={handleView} variant="Secondary" fill="Soft">
|
|
||||||
<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>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
</OverlayCenter>
|
</OverlayCenter>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
<Button
|
<NavItem variant="Background" aria-selected={selected}>
|
||||||
variant="Secondary"
|
<NavButton onClick={() => setDialog(true)}>
|
||||||
fill="Soft"
|
<NavItemContent>{children}</NavItemContent>
|
||||||
size="300"
|
</NavButton>
|
||||||
before={<Icon size="100" src={Icons.Plus} />}
|
</NavItem>
|
||||||
onClick={() => setDialog(true)}
|
|
||||||
>
|
|
||||||
<Text size="B300" truncate>
|
|
||||||
Add Server
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExploreServerNavItemAction = {
|
||||||
|
onClick: () => Promise<void>;
|
||||||
|
icon: IconSrc;
|
||||||
|
alwaysVisible: boolean;
|
||||||
|
};
|
||||||
|
type ExploreServerNavItemProps = {
|
||||||
|
server: string;
|
||||||
|
selected: boolean;
|
||||||
|
icon: IconSrc;
|
||||||
|
action?: ExploreServerNavItemAction;
|
||||||
|
};
|
||||||
|
export function ExploreServerNavItem({
|
||||||
|
server,
|
||||||
|
selected,
|
||||||
|
icon,
|
||||||
|
action,
|
||||||
|
}: ExploreServerNavItemProps) {
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
|
const { hoverProps } = useHover({ onHoverChange: 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 (
|
||||||
|
<NavItem
|
||||||
|
variant="Background"
|
||||||
|
radii="400"
|
||||||
|
aria-selected={selected}
|
||||||
|
{...hoverProps}
|
||||||
|
{...focusWithinProps}
|
||||||
|
>
|
||||||
|
<NavLink to={getExploreServerPath(server)}>
|
||||||
|
<NavItemContent>
|
||||||
|
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||||
|
<Avatar size="200" radii="400">
|
||||||
|
<Icon src={icon} size="100" filled={selected} />
|
||||||
|
</Avatar>
|
||||||
|
<Box as="span" grow="Yes">
|
||||||
|
<Text as="span" size="Inherit" truncate>
|
||||||
|
{server}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</NavItemContent>
|
||||||
|
</NavLink>
|
||||||
|
{action !== undefined && (hover || actionInProgress || action.alwaysVisible) && (
|
||||||
|
<NavItemOptions>
|
||||||
|
<IconButton
|
||||||
|
onClick={actionCallback}
|
||||||
|
variant="Background"
|
||||||
|
fill="None"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
disabled={actionInProgress}
|
||||||
|
>
|
||||||
|
{actionInProgress ? (
|
||||||
|
<Spinner variant="Secondary" fill="Solid" size="200" />
|
||||||
|
) : (
|
||||||
|
<Icon size="50" src={action.icon} />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</NavItemOptions>
|
||||||
|
)}
|
||||||
|
</NavItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Explore() {
|
export function Explore() {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
useNavToActivePathMapper('explore');
|
useNavToActivePathMapper('explore');
|
||||||
const userId = mx.getUserId();
|
const userId = mx.getUserId();
|
||||||
const clientConfig = useClientConfig();
|
const clientConfig = useClientConfig();
|
||||||
const userServer = userId ? getMxIdServer(userId) : undefined;
|
const userServer = userId ? getMxIdServer(userId) : undefined;
|
||||||
const servers =
|
const featuredServers = useMemo(
|
||||||
clientConfig.featuredCommunities?.servers?.filter((server) => server !== userServer) ?? [];
|
() =>
|
||||||
|
clientConfig.featuredCommunities?.servers?.filter((server) => server !== userServer) ?? [],
|
||||||
|
[clientConfig, userServer]
|
||||||
|
);
|
||||||
|
const [exploreServers, addServer, removeServer] = useExploreServers(featuredServers);
|
||||||
|
|
||||||
const featuredSelected = useExploreFeaturedSelected();
|
|
||||||
const selectedServer = useExploreServer();
|
const selectedServer = useExploreServer();
|
||||||
|
const exploringFeaturedRooms = useExploreFeaturedRooms();
|
||||||
|
const exploringUnlistedServer = useMemo(
|
||||||
|
() =>
|
||||||
|
!(
|
||||||
|
selectedServer === undefined ||
|
||||||
|
selectedServer === userServer ||
|
||||||
|
featuredServers.includes(selectedServer) ||
|
||||||
|
exploreServers.includes(selectedServer)
|
||||||
|
),
|
||||||
|
[exploreServers, selectedServer, userServer, featuredServers]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addServerCallback = useCallback(
|
||||||
|
async (server: string, save: boolean) => {
|
||||||
|
if (save && server !== userServer && !featuredServers.includes(server) && selectedServer) {
|
||||||
|
await addServer(server);
|
||||||
|
}
|
||||||
|
navigate(getExploreServerPath(server));
|
||||||
|
},
|
||||||
|
[addServer, navigate, userServer, featuredServers, selectedServer]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeServerCallback = useCallback(
|
||||||
|
async (server: string) => {
|
||||||
|
await removeServer(server);
|
||||||
|
},
|
||||||
|
[removeServer]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageNav>
|
<PageNav>
|
||||||
|
@ -182,12 +300,12 @@ export function Explore() {
|
||||||
<PageNavContent>
|
<PageNavContent>
|
||||||
<Box direction="Column" gap="300">
|
<Box direction="Column" gap="300">
|
||||||
<NavCategory>
|
<NavCategory>
|
||||||
<NavItem variant="Background" radii="400" aria-selected={featuredSelected}>
|
<NavItem variant="Background" radii="400" aria-selected={exploringFeaturedRooms}>
|
||||||
<NavLink to={getExploreFeaturedPath()}>
|
<NavLink to={getExploreFeaturedPath()}>
|
||||||
<NavItemContent>
|
<NavItemContent>
|
||||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||||
<Avatar size="200" radii="400">
|
<Avatar size="200" radii="400">
|
||||||
<Icon src={Icons.Bulb} size="100" filled={featuredSelected} />
|
<Icon src={Icons.Bulb} size="100" filled={exploringFeaturedRooms} />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box as="span" grow="Yes">
|
<Box as="span" grow="Yes">
|
||||||
<Text as="span" size="Inherit" truncate>
|
<Text as="span" size="Inherit" truncate>
|
||||||
|
@ -199,67 +317,68 @@ export function Explore() {
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
{userServer && (
|
{userServer && (
|
||||||
<NavItem
|
<ExploreServerNavItem
|
||||||
variant="Background"
|
server={userServer}
|
||||||
radii="400"
|
selected={userServer === selectedServer}
|
||||||
aria-selected={selectedServer === userServer}
|
icon={Icons.Home}
|
||||||
>
|
/>
|
||||||
<NavLink to={getExploreServerPath(userServer)}>
|
)}
|
||||||
<NavItemContent>
|
{exploringUnlistedServer && selectedServer !== undefined && (
|
||||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
<ExploreServerNavItem
|
||||||
<Avatar size="200" radii="400">
|
server={selectedServer}
|
||||||
<Icon
|
selected
|
||||||
src={Icons.Server}
|
icon={Icons.Server}
|
||||||
size="100"
|
action={{
|
||||||
filled={selectedServer === userServer}
|
alwaysVisible: true,
|
||||||
/>
|
icon: Icons.Plus,
|
||||||
</Avatar>
|
onClick: () => addServerCallback(selectedServer, true),
|
||||||
<Box as="span" grow="Yes">
|
}}
|
||||||
<Text as="span" size="Inherit" truncate>
|
/>
|
||||||
{userServer}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</NavItemContent>
|
|
||||||
</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
)}
|
)}
|
||||||
</NavCategory>
|
</NavCategory>
|
||||||
{servers.length > 0 && (
|
<NavCategory>
|
||||||
<NavCategory>
|
<NavCategoryHeader>
|
||||||
<NavCategoryHeader>
|
<Text size="O400" style={{ paddingLeft: config.space.S200 }}>
|
||||||
<Text size="O400" style={{ paddingLeft: config.space.S200 }}>
|
Servers
|
||||||
Servers
|
</Text>
|
||||||
</Text>
|
</NavCategoryHeader>
|
||||||
</NavCategoryHeader>
|
{featuredServers.map((server) => (
|
||||||
{servers.map((server) => (
|
<ExploreServerNavItem
|
||||||
<NavItem
|
key={server}
|
||||||
key={server}
|
server={server}
|
||||||
variant="Background"
|
selected={server === selectedServer}
|
||||||
radii="400"
|
icon={Icons.Server}
|
||||||
aria-selected={server === selectedServer}
|
/>
|
||||||
>
|
))}
|
||||||
<NavLink to={getExploreServerPath(server)}>
|
{exploreServers.map((server) => (
|
||||||
<NavItemContent>
|
<ExploreServerNavItem
|
||||||
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
key={server}
|
||||||
<Avatar size="200" radii="400">
|
server={server}
|
||||||
<Icon src={Icons.Server} size="100" filled={server === selectedServer} />
|
selected={server === selectedServer}
|
||||||
</Avatar>
|
icon={Icons.Server}
|
||||||
<Box as="span" grow="Yes">
|
action={{
|
||||||
<Text as="span" size="Inherit" truncate>
|
alwaysVisible: false,
|
||||||
{server}
|
icon: Icons.Minus,
|
||||||
</Text>
|
onClick: () => removeServerCallback(server),
|
||||||
</Box>
|
}}
|
||||||
</Box>
|
/>
|
||||||
</NavItemContent>
|
))}
|
||||||
</NavLink>
|
<AddExploreServerPrompt
|
||||||
</NavItem>
|
onSubmit={addServerCallback}
|
||||||
))}
|
header={<Text size="H4">Add Server</Text>}
|
||||||
</NavCategory>
|
>
|
||||||
)}
|
<Box as="span" grow="Yes" alignItems="Center" gap="200">
|
||||||
<Box direction="Column">
|
<Avatar size="200" radii="400">
|
||||||
<AddServer />
|
<Icon src={Icons.Plus} size="100" />
|
||||||
</Box>
|
</Avatar>
|
||||||
|
<Box as="span" grow="Yes">
|
||||||
|
<Text as="span" size="Inherit" truncate>
|
||||||
|
Add Server
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</AddExploreServerPrompt>
|
||||||
|
</NavCategory>
|
||||||
</Box>
|
</Box>
|
||||||
</PageNavContent>
|
</PageNavContent>
|
||||||
</PageNav>
|
</PageNav>
|
||||||
|
|
|
@ -24,6 +24,8 @@ import {
|
||||||
Scroll,
|
Scroll,
|
||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
TooltipProvider,
|
||||||
config,
|
config,
|
||||||
toRem,
|
toRem,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
|
@ -45,6 +47,8 @@ import { getMxIdServer } from '../../../utils/matrix';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
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 { useExploreServers } from '../../../hooks/useExploreServers';
|
||||||
|
|
||||||
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
|
const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams =>
|
||||||
useMemo(
|
useMemo(
|
||||||
|
@ -348,6 +352,7 @@ 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);
|
||||||
|
@ -356,6 +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 isUserAddedServer = server && exploreServers.includes(server);
|
||||||
|
const isUserHomeServer = server && server === userServer;
|
||||||
|
|
||||||
const currentLimit: number = useMemo(() => {
|
const currentLimit: number = useMemo(() => {
|
||||||
const limitParam = serverSearchParams.limit;
|
const limitParam = serverSearchParams.limit;
|
||||||
|
@ -468,6 +476,20 @@ export function PublicRooms() {
|
||||||
explore({ instance: instanceId, since: undefined });
|
explore({ instance: instanceId, since: undefined });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
|
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
||||||
|
};
|
||||||
|
|
||||||
|
const [removeServerState, handleRemoveServer] = useAsyncCallback(
|
||||||
|
useCallback(async () => {
|
||||||
|
if (!server) return;
|
||||||
|
|
||||||
|
setMenuAnchor(undefined);
|
||||||
|
await removeServer(server);
|
||||||
|
}, [server, removeServer])
|
||||||
|
);
|
||||||
|
const isRemoving = removeServerState.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader balance>
|
<PageHeader balance>
|
||||||
|
@ -506,13 +528,78 @@ export function PublicRooms() {
|
||||||
</BackRouteHandler>
|
</BackRouteHandler>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box grow="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={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>
|
||||||
</Box>
|
</Box>
|
||||||
<Box grow="Yes" basis="No" />
|
<Box shrink="No" grow="Yes" basis="No" justifyContent="End">
|
||||||
|
<TooltipProvider
|
||||||
|
position="Bottom"
|
||||||
|
align="End"
|
||||||
|
offset={4}
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text>More Options</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(triggerRef) =>
|
||||||
|
isUserAddedServer && (
|
||||||
|
<IconButton
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
ref={triggerRef}
|
||||||
|
aria-pressed={!!menuAnchor}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
|
@ -4,6 +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',
|
||||||
|
|
||||||
ElementRecentEmoji = 'io.element.recent_emoji',
|
ElementRecentEmoji = 'io.element.recent_emoji',
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue