Add new join with address prompt (#2442)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Sat, 16 Aug 2025 11:40:39 +0000 (17:10 +0530)
committerGitHub <noreply@github.com>
Sat, 16 Aug 2025 11:40:39 +0000 (21:40 +1000)
src/app/components/join-address-prompt/JoinAddressPrompt.tsx [new file with mode: 0644]
src/app/components/join-address-prompt/index.ts [new file with mode: 0644]
src/app/pages/client/home/Home.tsx
src/app/pages/client/sidebar/CreateTab.tsx

diff --git a/src/app/components/join-address-prompt/JoinAddressPrompt.tsx b/src/app/components/join-address-prompt/JoinAddressPrompt.tsx
new file mode 100644 (file)
index 0000000..50a8941
--- /dev/null
@@ -0,0 +1,131 @@
+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>
+  );
+}
diff --git a/src/app/components/join-address-prompt/index.ts b/src/app/components/join-address-prompt/index.ts
new file mode 100644 (file)
index 0000000..b14b8a6
--- /dev/null
@@ -0,0 +1 @@
+export * from './JoinAddressPrompt';
index d23339199b30b54040a40e8b511ebc270d5428f0..2597bb73751480b25bd5bd7a5f41fe5ad968f122 100644 (file)
@@ -30,10 +30,12 @@ import {
   NavLink,
 } from '../../../components/nav';
 import {
+  encodeSearchParamValueArray,
   getExplorePath,
   getHomeCreatePath,
   getHomeRoomPath,
   getHomeSearchPath,
+  withSearchParam,
 } from '../../pathUtils';
 import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
 import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
@@ -49,7 +51,6 @@ import { makeNavCategoryId } from '../../../state/closedNavCategories';
 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';
@@ -61,6 +62,9 @@ import {
   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;
@@ -77,11 +81,6 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
     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 }}>
@@ -96,16 +95,6 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
             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>
   );
@@ -268,22 +257,44 @@ export function Home() {
                   </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>
index a7f9350c0aca1229549829adc3bc187137dc3154..e6575cb438621c7bdfccb00423acd8ce0fbf3f9e 100644 (file)
@@ -7,15 +7,22 @@ import { stopPropagation } from '../../../utils/keyboard';
 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());
@@ -27,7 +34,7 @@ export function CreateTab() {
   };
 
   const handleJoinWithAddress = () => {
-    openJoinAlias();
+    setJoinAddress(true);
     setMenuCords(undefined);
   };
 
@@ -103,6 +110,22 @@ export function CreateTab() {
             >
               <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>