--- /dev/null
+import React, { FormEventHandler, useState } from 'react';
+import FocusTrap from 'focus-trap-react';
+import {
+ Dialog,
+ Overlay,
+ OverlayCenter,
+ OverlayBackdrop,
+ Header,
+ config,
+ Box,
+ Text,
+ IconButton,
+ Icon,
+ Icons,
+ Button,
+ Input,
+ color,
+} from 'folds';
+import { stopPropagation } from '../../utils/keyboard';
+import { isRoomAlias, isRoomId } from '../../utils/matrix';
+import { parseMatrixToRoom, parseMatrixToRoomEvent, testMatrixTo } from '../../plugins/matrix-to';
+import { tryDecodeURIComponent } from '../../utils/dom';
+
+type JoinAddressProps = {
+ onOpen: (roomIdOrAlias: string, via?: string[], eventId?: string) => void;
+ onCancel: () => void;
+};
+export function JoinAddressPrompt({ onOpen, onCancel }: JoinAddressProps) {
+ const [invalid, setInvalid] = useState(false);
+
+ const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
+ evt.preventDefault();
+ setInvalid(false);
+
+ const target = evt.target as HTMLFormElement | undefined;
+ const addressInput = target?.addressInput as HTMLInputElement | undefined;
+ const address = addressInput?.value.trim();
+ if (!address) return;
+
+ if (isRoomId(address) || isRoomAlias(address)) {
+ onOpen(address);
+ return;
+ }
+
+ if (testMatrixTo(address)) {
+ const decodedAddress = tryDecodeURIComponent(address);
+ const toRoom = parseMatrixToRoom(decodedAddress);
+ if (toRoom) {
+ onOpen(toRoom.roomIdOrAlias, toRoom.viaServers);
+ return;
+ }
+
+ const toEvent = parseMatrixToRoomEvent(decodedAddress);
+ if (toEvent) {
+ onOpen(toEvent.roomIdOrAlias, toEvent.viaServers, toEvent.eventId);
+ return;
+ }
+ }
+
+ setInvalid(true);
+ };
+
+ return (
+ <Overlay open backdrop={<OverlayBackdrop />}>
+ <OverlayCenter>
+ <FocusTrap
+ focusTrapOptions={{
+ initialFocus: false,
+ onDeactivate: onCancel,
+ clickOutsideDeactivates: true,
+ escapeDeactivates: stopPropagation,
+ }}
+ >
+ <Dialog variant="Surface">
+ <Header
+ style={{
+ padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+ }}
+ variant="Surface"
+ size="500"
+ >
+ <Box grow="Yes">
+ <Text size="H4">Join with Address</Text>
+ </Box>
+ <IconButton size="300" onClick={onCancel} radii="300">
+ <Icon src={Icons.Cross} />
+ </IconButton>
+ </Header>
+ <Box
+ as="form"
+ onSubmit={handleSubmit}
+ style={{ padding: config.space.S400, paddingTop: 0 }}
+ direction="Column"
+ gap="400"
+ >
+ <Box direction="Column" gap="200">
+ <Text priority="400" size="T300">
+ Enter public address to join the community. Addresses looks like:
+ </Text>
+ <Text as="ul" size="T200" priority="300" style={{ paddingLeft: config.space.S400 }}>
+ <li>#community:server</li>
+ <li>https://matrix.to/#/#community:server</li>
+ <li>https://matrix.to/#/!xYzAj?via=server</li>
+ </Text>
+ </Box>
+ <Box direction="Column" gap="100">
+ <Text size="L400">Address</Text>
+ <Input
+ size="500"
+ autoFocus
+ name="addressInput"
+ variant="Background"
+ placeholder="#community:server"
+ required
+ />
+ {invalid && (
+ <Text size="T200" style={{ color: color.Critical.Main }}>
+ <b>Invalid Address</b>
+ </Text>
+ )}
+ </Box>
+ <Button type="submit" variant="Primary">
+ <Text size="B400">Open</Text>
+ </Button>
+ </Box>
+ </Dialog>
+ </FocusTrap>
+ </OverlayCenter>
+ </Overlay>
+ );
+}
--- /dev/null
+export * from './JoinAddressPrompt';
NavLink,
} from '../../../components/nav';
import {
+ encodeSearchParamValueArray,
getExplorePath,
getHomeCreatePath,
getHomeRoomPath,
getHomeSearchPath,
+ withSearchParam,
} from '../../pathUtils';
import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
-import { openJoinAlias } from '../../../../client/action/navigation';
import { PageNav, PageNavHeader, PageNavContent } from '../../../components/page';
import { useRoomsUnread } from '../../../state/hooks/unread';
import { markAsRead } from '../../../../client/action/notifications';
getRoomNotificationMode,
useRoomsNotificationPreferencesContext,
} from '../../../hooks/useRoomsNotificationPreferences';
+import { UseStateProvider } from '../../../components/UseStateProvider';
+import { JoinAddressPrompt } from '../../../components/join-address-prompt';
+import { _RoomSearchParams } from '../../paths';
type HomeMenuProps = {
requestClose: () => void;
requestClose();
};
- const handleJoinAddress = () => {
- openJoinAlias();
- requestClose();
- };
-
return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
Mark as Read
</Text>
</MenuItem>
- <MenuItem
- onClick={handleJoinAddress}
- size="300"
- radii="300"
- after={<Icon size="100" src={Icons.Link} />}
- >
- <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
- Join with Address
- </Text>
- </MenuItem>
</Box>
</Menu>
);
</NavItemContent>
</NavButton>
</NavItem>
- <NavItem variant="Background" radii="400">
- <NavButton onClick={() => openJoinAlias()}>
- <NavItemContent>
- <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>
- Join with Address
- </Text>
- </Box>
- </Box>
- </NavItemContent>
- </NavButton>
- </NavItem>
+ <UseStateProvider initial={false}>
+ {(open, setOpen) => (
+ <>
+ <NavItem variant="Background" radii="400">
+ <NavButton onClick={() => setOpen(true)}>
+ <NavItemContent>
+ <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>
+ Join with Address
+ </Text>
+ </Box>
+ </Box>
+ </NavItemContent>
+ </NavButton>
+ </NavItem>
+ {open && (
+ <JoinAddressPrompt
+ onCancel={() => setOpen(false)}
+ onOpen={(roomIdOrAlias, viaServers, eventId) => {
+ setOpen(false);
+ const path = getHomeRoomPath(roomIdOrAlias, eventId);
+ navigate(
+ viaServers
+ ? withSearchParam<_RoomSearchParams>(path, {
+ viaServers: encodeSearchParamValueArray(viaServers),
+ })
+ : path
+ );
+ }}
+ />
+ )}
+ </>
+ )}
+ </UseStateProvider>
<NavItem variant="Background" radii="400" aria-selected={searchSelected}>
<NavLink to={getHomeSearchPath()}>
<NavItemContent>
import { SequenceCard } from '../../../components/sequence-card';
import { SettingTile } from '../../../components/setting-tile';
import { ContainerColor } from '../../../styles/ContainerColor.css';
-import { openJoinAlias } from '../../../../client/action/navigation';
-import { getCreatePath } from '../../pathUtils';
+import {
+ encodeSearchParamValueArray,
+ getCreatePath,
+ getSpacePath,
+ withSearchParam,
+} from '../../pathUtils';
import { useCreateSelected } from '../../../hooks/router/useCreateSelected';
+import { JoinAddressPrompt } from '../../../components/join-address-prompt';
+import { _RoomSearchParams } from '../../paths';
export function CreateTab() {
const createSelected = useCreateSelected();
const navigate = useNavigate();
const [menuCords, setMenuCords] = useState<RectCords>();
+ const [joinAddress, setJoinAddress] = useState(false);
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuCords(menuCords ? undefined : evt.currentTarget.getBoundingClientRect());
};
const handleJoinWithAddress = () => {
- openJoinAlias();
+ setJoinAddress(true);
setMenuCords(undefined);
};
>
<Icon src={Icons.Plus} />
</SidebarAvatar>
+ {joinAddress && (
+ <JoinAddressPrompt
+ onCancel={() => setJoinAddress(false)}
+ onOpen={(roomIdOrAlias, viaServers) => {
+ setJoinAddress(false);
+ const path = getSpacePath(roomIdOrAlias);
+ navigate(
+ viaServers
+ ? withSearchParam<_RoomSearchParams>(path, {
+ viaServers: encodeSearchParamValueArray(viaServers),
+ })
+ : path
+ );
+ }}
+ />
+ )}
</PopOut>
)}
</SidebarItemTooltip>