handle additional creators in room upgrade

This commit is contained in:
Ajay Bura 2025-08-12 18:42:57 +05:30
parent 6763721983
commit 1594a2ccd1
2 changed files with 139 additions and 90 deletions

View file

@ -34,9 +34,11 @@ import { findAndReplace } from '../../utils/findAndReplace';
import { highlightText } from '../../styles/CustomHtml.css'; import { highlightText } from '../../styles/CustomHtml.css';
import { makeHighlightRegex } from '../../plugins/react-custom-html-parser'; import { makeHighlightRegex } from '../../plugins/react-custom-html-parser';
export const useAdditionalCreators = () => { export const useAdditionalCreators = (defaultCreators?: string[]) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [additionalCreators, setAdditionalCreators] = useState<string[]>([]); const [additionalCreators, setAdditionalCreators] = useState<string[]>(
() => defaultCreators?.filter((id) => id !== mx.getSafeUserId()) ?? []
);
const addAdditionalCreator = (userId: string) => { const addAdditionalCreator = (userId: string) => {
if (userId === mx.getSafeUserId()) return; if (userId === mx.getSafeUserId()) return;
@ -75,11 +77,13 @@ type AdditionalCreatorInputProps = {
additionalCreators: string[]; additionalCreators: string[];
onSelect: (userId: string) => void; onSelect: (userId: string) => void;
onRemove: (userId: string) => void; onRemove: (userId: string) => void;
disabled?: boolean;
}; };
export function AdditionalCreatorInput({ export function AdditionalCreatorInput({
additionalCreators, additionalCreators,
onSelect, onSelect,
onRemove, onRemove,
disabled,
}: AdditionalCreatorInputProps) { }: AdditionalCreatorInputProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [menuCords, setMenuCords] = useState<RectCords>(); const [menuCords, setMenuCords] = useState<RectCords>();
@ -164,6 +168,7 @@ export function AdditionalCreatorInput({
radii="Pill" radii="Pill"
after={<Icon size="50" src={Icons.Cross} />} after={<Icon size="50" src={Icons.Cross} />}
onClick={() => onRemove(creator)} onClick={() => onRemove(creator)}
disabled={disabled}
> >
<Text size="B300">{creator}</Text> <Text size="B300">{creator}</Text>
</Chip> </Chip>
@ -289,6 +294,7 @@ export function AdditionalCreatorInput({
radii="Pill" radii="Pill"
onClick={handleOpenMenu} onClick={handleOpenMenu}
aria-pressed={!!menuCords} aria-pressed={!!menuCords}
disabled={disabled}
> >
<Icon size="50" src={Icons.Plus} /> <Icon size="50" src={Icons.Plus} />
</Chip> </Chip>

View file

@ -1,4 +1,4 @@
import React, { FormEventHandler, useCallback, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { import {
Button, Button,
color, color,
@ -14,10 +14,9 @@ import {
IconButton, IconButton,
Icon, Icon,
Icons, Icons,
Input,
} from 'folds'; } from 'folds';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { MatrixError } from 'matrix-js-sdk'; import { MatrixError, Method } from 'matrix-js-sdk';
import { RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types'; import { RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SequenceCardStyle } from '../../room-settings/styles.css';
@ -31,6 +30,133 @@ import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { useCapabilities } from '../../../hooks/useCapabilities'; import { useCapabilities } from '../../../hooks/useCapabilities';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
import {
AdditionalCreatorInput,
RoomVersionSelector,
useAdditionalCreators,
} from '../../../components/create-room';
import { useAlive } from '../../../hooks/useAlive';
import { creatorsSupported } from '../../../utils/matrix';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { BreakWord } from '../../../styles/Text.css';
function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
const mx = useMatrixClient();
const room = useRoom();
const alive = useAlive();
const creators = useRoomCreators(room);
const capabilities = useCapabilities();
const roomVersions = capabilities['m.room_versions'];
const [selectedRoomVersion, selectRoomVersion] = useState(roomVersions?.default ?? '1');
useEffect(() => {
// capabilities load async
selectRoomVersion(roomVersions?.default ?? '1');
}, [roomVersions?.default]);
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
useAdditionalCreators(Array.from(creators));
const [upgradeState, upgrade] = useAsyncCallback(
useCallback(
async (version: string, newAdditionalCreators?: string[]) => {
await mx.http.authedRequest(Method.Post, `/rooms/${room.roomId}/upgrade`, undefined, {
new_version: version,
additional_creators: newAdditionalCreators,
});
},
[mx, room]
)
);
const upgrading = upgradeState.status === AsyncStatus.Loading;
const handleUpgradeRoom = () => {
const version = selectedRoomVersion;
upgrade(version, allowAdditionalCreators ? additionalCreators : undefined).then(() => {
if (alive()) {
requestClose();
}
});
};
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: requestClose,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<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.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}</Text>
</Box>
<IconButton size="300" onClick={requestClose} 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">Options</Text>
<RoomVersionSelector
versions={roomVersions?.available ? Object.keys(roomVersions.available) : ['1']}
value={selectedRoomVersion}
onChange={selectRoomVersion}
disabled={upgrading}
/>
{allowAdditionalCreators && (
<SequenceCard
style={{ padding: config.space.S300 }}
variant="SurfaceVariant"
direction="Column"
gap="500"
>
<AdditionalCreatorInput
additionalCreators={additionalCreators}
onSelect={addAdditionalCreator}
onRemove={removeAdditionalCreator}
disabled={upgrading}
/>
</SequenceCard>
)}
</Box>
{upgradeState.status === AsyncStatus.Error && (
<Text className={BreakWord} style={{ color: color.Critical.Main }} size="T200">
{(upgradeState.error as MatrixError).message}
</Text>
)}
<Button
onClick={handleUpgradeRoom}
variant="Secondary"
disabled={upgrading}
before={upgrading && <Spinner size="200" variant="Secondary" fill="Solid" />}
>
<Text size="B400">{room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}</Text>
</Button>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}
type RoomUpgradeProps = { type RoomUpgradeProps = {
permissions: RoomPermissionsAPI; permissions: RoomPermissionsAPI;
@ -47,9 +173,6 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
const roomVersion = createContent?.room_version ?? '1'; const roomVersion = createContent?.room_version ?? '1';
const predecessorRoomId = createContent?.predecessor?.room_id; const predecessorRoomId = createContent?.predecessor?.room_id;
const capabilities = useCapabilities();
const defaultRoomVersion = capabilities['m.room_versions']?.default;
const tombstoneContent = useStateEvent( const tombstoneContent = useStateEvent(
room, room,
StateEvent.RoomTombstone StateEvent.RoomTombstone
@ -80,31 +203,8 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
} }
}; };
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 [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 ( return (
<SequenceCard <SequenceCard
className={SequenceCardStyle} className={SequenceCardStyle}
@ -150,8 +250,7 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
variant="Secondary" variant="Secondary"
fill="Solid" fill="Solid"
radii="300" radii="300"
disabled={upgrading || !canUpgrade} disabled={!canUpgrade}
before={upgrading && <Spinner size="100" variant="Secondary" fill="Solid" />}
onClick={() => setPrompt(true)} onClick={() => setPrompt(true)}
> >
<Text size="B300">Upgrade</Text> <Text size="B300">Upgrade</Text>
@ -160,63 +259,7 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
</Box> </Box>
} }
> >
{upgradeState.status === AsyncStatus.Error && ( {prompt && <RoomUpgradeDialog requestClose={() => setPrompt(false)} />}
<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.isSpaceRoom() ? 'Space Upgrade' : '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">
{room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}
</Text>
</Button>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
)}
</SettingTile> </SettingTile>
</SequenceCard> </SequenceCard>
); );