Add room upgrade option in room settings (#2280)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Thu, 20 Mar 2025 09:23:16 +0000 (20:23 +1100)
committerGitHub <noreply@github.com>
Thu, 20 Mar 2025 09:23:16 +0000 (20:23 +1100)
* add room upgrade option in room settings

* update upgrade room dialog styles

src/app/features/room-settings/general/General.tsx
src/app/features/room-settings/general/RoomUpgrade.tsx [new file with mode: 0644]

index 6d66406a8a90fd871fe8fa675a75ad91eb5af564..1033c300d63c1111a2515fa911717ff47675fd64 100644 (file)
@@ -8,6 +8,7 @@ import { RoomEncryption } from './RoomEncryption';
 import { RoomHistoryVisibility } from './RoomHistoryVisibility';
 import { RoomJoinRules } from './RoomJoinRules';
 import { RoomLocalAddresses, RoomPublishedAddresses } from './RoomAddress';
+import { RoomUpgrade } from './RoomUpgrade';
 
 type GeneralProps = {
   requestClose: () => void;
@@ -48,6 +49,10 @@ export function General({ requestClose }: GeneralProps) {
                 <RoomPublishedAddresses powerLevels={powerLevels} />
                 <RoomLocalAddresses powerLevels={powerLevels} />
               </Box>
+              <Box direction="Column" gap="100">
+                <Text size="L400">Advance Options</Text>
+                <RoomUpgrade powerLevels={powerLevels} requestClose={requestClose} />
+              </Box>
             </Box>
           </PageContent>
         </Scroll>
diff --git a/src/app/features/room-settings/general/RoomUpgrade.tsx b/src/app/features/room-settings/general/RoomUpgrade.tsx
new file mode 100644 (file)
index 0000000..fa1fa85
--- /dev/null
@@ -0,0 +1,217 @@
+import React, { FormEventHandler, useCallback, useState } from 'react';
+import {
+  Button,
+  color,
+  Spinner,
+  Text,
+  Overlay,
+  OverlayBackdrop,
+  OverlayCenter,
+  Dialog,
+  Header,
+  config,
+  Box,
+  IconButton,
+  Icon,
+  Icons,
+  Input,
+} from 'folds';
+import FocusTrap from 'focus-trap-react';
+import { MatrixError } from 'matrix-js-sdk';
+import { RoomCreateEventContent, RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types';
+import { SequenceCard } from '../../../components/sequence-card';
+import { SequenceCardStyle } from '../styles.css';
+import { SettingTile } from '../../../components/setting-tile';
+import { useRoom } from '../../../hooks/useRoom';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
+import { StateEvent } from '../../../../types/matrix/room';
+import { useMatrixClient } from '../../../hooks/useMatrixClient';
+import { useStateEvent } from '../../../hooks/useStateEvent';
+import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
+import { useCapabilities } from '../../../hooks/useCapabilities';
+import { stopPropagation } from '../../../utils/keyboard';
+
+type RoomUpgradeProps = {
+  powerLevels: IPowerLevels;
+  requestClose: () => void;
+};
+export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
+  const mx = useMatrixClient();
+  const room = useRoom();
+  const { navigateRoom } = useRoomNavigate();
+  const createContent = useStateEvent(
+    room,
+    StateEvent.RoomCreate
+  )?.getContent<RoomCreateEventContent>();
+  const roomVersion = createContent?.room_version ?? 1;
+  const predecessorRoomId = createContent?.predecessor?.room_id;
+
+  const capabilities = useCapabilities();
+  const defaultRoomVersion = capabilities['m.room_versions']?.default;
+
+  const tombstoneContent = useStateEvent(
+    room,
+    StateEvent.RoomTombstone
+  )?.getContent<RoomTombstoneEventContent>();
+  const replacementRoom = tombstoneContent?.replacement_room;
+
+  const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
+  const canUpgrade = powerLevelAPI.canSendStateEvent(
+    powerLevels,
+    StateEvent.RoomTombstone,
+    userPowerLevel
+  );
+
+  const handleOpenRoom = () => {
+    if (replacementRoom) {
+      requestClose();
+      navigateRoom(replacementRoom);
+    }
+  };
+
+  const handleOpenOldRoom = () => {
+    if (predecessorRoomId) {
+      requestClose();
+      navigateRoom(predecessorRoomId, createContent.predecessor?.event_id);
+    }
+  };
+
+  const [upgradeState, upgrade] = useAsyncCallback(
+    useCallback(
+      async (version: string) => {
+        await mx.upgradeRoom(room.roomId, version);
+      },
+      [mx, room]
+    )
+  );
+
+  const upgrading = upgradeState.status === AsyncStatus.Loading;
+
+  const [prompt, setPrompt] = useState(false);
+
+  const handleSubmitUpgrade: FormEventHandler<HTMLFormElement> = (evt) => {
+    evt.preventDefault();
+
+    const target = evt.target as HTMLFormElement | undefined;
+    const versionInput = target?.versionInput as HTMLInputElement | undefined;
+    const version = versionInput?.value.trim();
+    if (!version) return;
+
+    upgrade(version);
+    setPrompt(false);
+  };
+
+  return (
+    <SequenceCard
+      className={SequenceCardStyle}
+      variant="SurfaceVariant"
+      direction="Column"
+      gap="400"
+    >
+      <SettingTile
+        title="Upgrade Room"
+        description={
+          replacementRoom
+            ? tombstoneContent.body || 'This room has been replaced!'
+            : `Current room version: ${roomVersion}.`
+        }
+        after={
+          <Box alignItems="Center" gap="200">
+            {predecessorRoomId && (
+              <Button
+                size="300"
+                variant="Secondary"
+                fill="Soft"
+                outlined
+                radii="300"
+                onClick={handleOpenOldRoom}
+              >
+                <Text size="B300">Old Room</Text>
+              </Button>
+            )}
+            {replacementRoom ? (
+              <Button
+                size="300"
+                variant="Success"
+                fill="Solid"
+                radii="300"
+                onClick={handleOpenRoom}
+              >
+                <Text size="B300">Open New Room</Text>
+              </Button>
+            ) : (
+              <Button
+                size="300"
+                variant="Secondary"
+                fill="Solid"
+                radii="300"
+                disabled={upgrading || !canUpgrade}
+                before={upgrading && <Spinner size="100" variant="Secondary" fill="Solid" />}
+                onClick={() => setPrompt(true)}
+              >
+                <Text size="B300">Upgrade</Text>
+              </Button>
+            )}
+          </Box>
+        }
+      >
+        {upgradeState.status === AsyncStatus.Error && (
+          <Text style={{ color: color.Critical.Main }} size="T200">
+            {(upgradeState.error as MatrixError).message}
+          </Text>
+        )}
+
+        {prompt && (
+          <Overlay open backdrop={<OverlayBackdrop />}>
+            <OverlayCenter>
+              <FocusTrap
+                focusTrapOptions={{
+                  initialFocus: false,
+                  onDeactivate: () => setPrompt(false),
+                  clickOutsideDeactivates: true,
+                  escapeDeactivates: stopPropagation,
+                }}
+              >
+                <Dialog variant="Surface" as="form" onSubmit={handleSubmitUpgrade}>
+                  <Header
+                    style={{
+                      padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
+                      borderBottomWidth: config.borderWidth.B300,
+                    }}
+                    variant="Surface"
+                    size="500"
+                  >
+                    <Box grow="Yes">
+                      <Text size="H4">Room Upgrade</Text>
+                    </Box>
+                    <IconButton size="300" onClick={() => setPrompt(false)} radii="300">
+                      <Icon src={Icons.Cross} />
+                    </IconButton>
+                  </Header>
+                  <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
+                    <Text priority="400" style={{ color: color.Critical.Main }}>
+                      <b>This action is irreversible!</b>
+                    </Text>
+                    <Box direction="Column" gap="100">
+                      <Text size="L400">Version</Text>
+                      <Input
+                        defaultValue={defaultRoomVersion}
+                        name="versionInput"
+                        variant="Background"
+                        required
+                      />
+                    </Box>
+                    <Button type="submit" variant="Secondary">
+                      <Text size="B400">Upgrade Room</Text>
+                    </Button>
+                  </Box>
+                </Dialog>
+              </FocusTrap>
+            </OverlayCenter>
+          </Overlay>
+        )}
+      </SettingTile>
+    </SequenceCard>
+  );
+}