2025-08-12 19:42:30 +05:30
import React , { useCallback , useEffect , useState } from 'react' ;
2025-03-20 20:23:16 +11:00
import {
Button ,
color ,
Spinner ,
Text ,
Overlay ,
OverlayBackdrop ,
OverlayCenter ,
Dialog ,
Header ,
config ,
Box ,
IconButton ,
Icon ,
Icons ,
} from 'folds' ;
import FocusTrap from 'focus-trap-react' ;
2025-08-12 19:42:30 +05:30
import { MatrixError , Method } from 'matrix-js-sdk' ;
import { RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types' ;
2025-03-20 20:23:16 +11:00
import { SequenceCard } from '../../../components/sequence-card' ;
2025-03-27 19:54:13 +11:00
import { SequenceCardStyle } from '../../room-settings/styles.css' ;
2025-03-20 20:23:16 +11:00
import { SettingTile } from '../../../components/setting-tile' ;
import { useRoom } from '../../../hooks/useRoom' ;
import { AsyncStatus , useAsyncCallback } from '../../../hooks/useAsyncCallback' ;
2025-08-12 19:42:30 +05:30
import { IRoomCreateContent , StateEvent } from '../../../../types/matrix/room' ;
2025-03-20 20:23:16 +11:00
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' ;
2025-08-12 19:42:30 +05:30
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' ;
2026-06-18 18:11:24 -04:00
import { useModalStyle } from '../../../hooks/useModalStyle' ;
2026-10-16 22:23:15 -04:00
import { stateToCarry } from '../../../utils/roomUpgradeState' ;
2025-08-12 19:42:30 +05:30
function RoomUpgradeDialog ({ requestClose } : { requestClose : () => void }) {
const mx = useMatrixClient ();
const room = useRoom ();
const alive = useAlive ();
const creators = useRoomCreators ( room );
2026-06-18 18:11:24 -04:00
const modalStyle = useModalStyle ( 480 );
2025-08-12 19:42:30 +05:30
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 []) => {
2026-10-16 22:23:15 -04:00
// [Gitea #327] Synapse doesn't copy Lotus room state (voice limit, call
// policy, soundboard, retention, role tags, emoji packs) into the
// replacement room: read it first, re-create it after the upgrade.
const carried = stateToCarry (
Array . from ( room . currentState . events . values ()). flatMap (( byKey ) =>
Array . from ( byKey . values ()),
),
);
const res = await mx . http . authedRequest < { replacement_room? : string } > (
Method . Post ,
`/rooms/ ${ room . roomId } /upgrade` ,
undefined ,
{
new_version : version ,
additional_creators : newAdditionalCreators ,
},
);
const newRoomId = res ? . replacement_room ;
if ( ! newRoomId || carried . length === 0 ) return ;
const results = await Promise . allSettled (
carried . map (( st ) =>
mx . http . authedRequest (
Method . Put ,
`/rooms/ ${ encodeURIComponent ( newRoomId ) } /state/ ${ encodeURIComponent ( st . type ) } / ${ encodeURIComponent ( st . stateKey ) } ` ,
undefined ,
st . content ,
),
),
);
const failed = results . filter (( r ) => r . status === 'rejected' ). length ;
if ( failed > 0 ) {
throw new Error (
`Room upgraded, but ${ failed } of ${ carried . length } Lotus settings (voice limit, call policy, soundboard, retention…) could not be copied to the new room.` ,
);
}
2025-08-12 19:42:30 +05:30
},
2026-05-21 23:30:50 -04:00
[ mx , room ],
),
2025-08-12 19:42:30 +05:30
);
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 = {{
2026-09-24 13:06:10 -04:00
fallbackFocus : '#roomupgrade-dialog-1' ,
2025-08-12 19:42:30 +05:30
onDeactivate : requestClose ,
clickOutsideDeactivates : true ,
escapeDeactivates : stopPropagation ,
}}
>
2026-09-24 13:06:10 -04:00
< Dialog
id = "roomupgrade-dialog-1"
role = "dialog"
aria-modal = "true"
aria-labelledby = "roomupgrade-dialog-1-title"
tabIndex = { - 1 }
variant = "Surface"
style = { modalStyle }
>
2025-08-12 19:42:30 +05:30
< Header
style = {{
padding : `0 ${ config . space . S200 } 0 ${ config . space . S400 } ` ,
borderBottomWidth : config.borderWidth.B300 ,
}}
variant = "Surface"
size = "500"
>
< Box grow = "Yes" >
2026-09-24 13:06:10 -04:00
< Text id = "roomupgrade-dialog-1-title" as = "h2" size = "H4" >
2026-05-21 20:49:33 -04:00
{ room . isSpaceRoom () ? 'Space Upgrade' : 'Room Upgrade' }
</ Text >
2025-08-12 19:42:30 +05:30
</ Box >
2026-05-21 13:17:07 -04:00
< IconButton size = "300" onClick = { requestClose } radii = "300" aria-label = "Close" >
2025-08-12 19:42:30 +05:30
< 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 >
);
}
2025-03-20 20:23:16 +11:00
type RoomUpgradeProps = {
2025-08-12 19:42:30 +05:30
permissions : RoomPermissionsAPI ;
2025-03-20 20:23:16 +11:00
requestClose : () => void ;
};
2025-08-12 19:42:30 +05:30
export function RoomUpgrade ({ permissions , requestClose } : RoomUpgradeProps ) {
2025-03-20 20:23:16 +11:00
const mx = useMatrixClient ();
const room = useRoom ();
2025-03-27 19:54:13 +11:00
const { navigateRoom , navigateSpace } = useRoomNavigate ();
2025-03-20 20:23:16 +11:00
const createContent = useStateEvent (
room ,
2026-05-21 23:30:50 -04:00
StateEvent . RoomCreate ,
2025-08-12 19:42:30 +05:30
) ? . getContent < IRoomCreateContent >();
const roomVersion = createContent ? . room_version ?? '1' ;
2025-03-20 20:23:16 +11:00
const predecessorRoomId = createContent ? . predecessor ? . room_id ;
const tombstoneContent = useStateEvent (
room ,
2026-05-21 23:30:50 -04:00
StateEvent . RoomTombstone ,
2025-03-20 20:23:16 +11:00
) ? . getContent < RoomTombstoneEventContent >();
const replacementRoom = tombstoneContent ? . replacement_room ;
2025-08-12 19:42:30 +05:30
const canUpgrade = permissions . stateEvent ( StateEvent . RoomTombstone , mx . getSafeUserId ());
2025-03-20 20:23:16 +11:00
const handleOpenRoom = () => {
if ( replacementRoom ) {
requestClose ();
2025-03-27 19:54:13 +11:00
if ( room . isSpaceRoom ()) {
navigateSpace ( replacementRoom );
} else {
navigateRoom ( replacementRoom );
}
2025-03-20 20:23:16 +11:00
}
};
const handleOpenOldRoom = () => {
if ( predecessorRoomId ) {
requestClose ();
2025-03-27 19:54:13 +11:00
if ( room . isSpaceRoom ()) {
navigateSpace ( predecessorRoomId );
} else {
navigateRoom ( predecessorRoomId , createContent . predecessor ? . event_id );
}
2025-03-20 20:23:16 +11:00
}
};
const [ prompt , setPrompt ] = useState ( false );
return (
< SequenceCard
className = { SequenceCardStyle }
variant = "SurfaceVariant"
direction = "Column"
gap = "400"
>
< SettingTile
2025-03-27 19:54:13 +11:00
title = { room . isSpaceRoom () ? 'Upgrade Space' : 'Upgrade Room' }
2025-03-20 20:23:16 +11:00
description = {
replacementRoom
2025-03-27 19:54:13 +11:00
? tombstoneContent . body ||
`This ${ room . isSpaceRoom () ? 'space' : 'room' } has been replaced!`
2025-08-12 19:42:30 +05:30
: `Current version: ${ roomVersion } .`
2025-03-20 20:23:16 +11:00
}
after = {
< Box alignItems = "Center" gap = "200" >
{ predecessorRoomId && (
< Button
size = "300"
variant = "Secondary"
fill = "Soft"
outlined
radii = "300"
onClick = { handleOpenOldRoom }
>
2025-03-27 19:54:13 +11:00
< Text size = "B300" >{ room . isSpaceRoom () ? 'Old Space' : 'Old Room' }</ Text >
2025-03-20 20:23:16 +11:00
</ Button >
)}
{ replacementRoom ? (
< Button
size = "300"
variant = "Success"
fill = "Solid"
radii = "300"
onClick = { handleOpenRoom }
>
2025-03-27 19:54:13 +11:00
< Text size = "B300" >{ room . isSpaceRoom () ? 'Open New Space' : 'Open New Room' }</ Text >
2025-03-20 20:23:16 +11:00
</ Button >
) : (
< Button
size = "300"
variant = "Secondary"
fill = "Solid"
radii = "300"
2025-08-12 19:42:30 +05:30
disabled = { ! canUpgrade }
2025-03-20 20:23:16 +11:00
onClick = {() => setPrompt ( true )}
>
< Text size = "B300" > Upgrade </ Text >
</ Button >
)}
</ Box >
}
>
2025-08-12 19:42:30 +05:30
{ prompt && < RoomUpgradeDialog requestClose = {() => setPrompt ( false )} />}
2025-03-20 20:23:16 +11:00
</ SettingTile >
</ SequenceCard >
);
}