feat: enhance update notification layout and add room info display in developer tools

This commit is contained in:
2026-02-24 21:49:03 +11:00
parent 495747af84
commit 58966aec19
5 changed files with 359 additions and 51 deletions

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import React, { useCallback, useMemo, useState } from 'react';
import { Box, Button, Icon, IconButton, Icons, Scroll, Spinner, Text, color } from 'folds';
import { useAtomValue } from 'jotai';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
@@ -9,6 +10,10 @@ import { usePermissionGroups } from './usePermissionItems';
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { useRecursiveChildScopeFactory, useRecursiveChildSpaceScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
type PermissionsProps = {
requestClose: () => void;
@@ -18,6 +23,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
const room = useRoom();
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const roomToParents = useAtomValue(roomToParentsAtom);
const permissions = useRoomPermissions(creators, powerLevels);
@@ -27,14 +33,110 @@ export function Permissions({ requestClose }: PermissionsProps) {
const [powerEditor, setPowerEditor] = useState(false);
// Get all child spaces recursively
const childSpaces = useSpaceChildren(
allRoomsAtom,
room.roomId,
useRecursiveChildSpaceScopeFactory(mx, roomToParents)
);
// Get all child rooms recursively
const childRooms = useSpaceChildren(
allRoomsAtom,
room.roomId,
useRecursiveChildScopeFactory(mx, roomToParents)
);
// Combine both spaces and rooms for syncing
const allChildren = [...childSpaces, ...childRooms];
// Calculate which children we can sync to
const { syncableChildren, nonSyncableChildren } = useMemo(() => {
const syncable: string[] = [];
const nonSyncable: { roomId: string; name: string }[] = [];
allChildren.forEach((childId) => {
const childRoom = mx.getRoom(childId);
if (!childRoom) {
nonSyncable.push({ roomId: childId, name: childId });
return;
}
// Check if we have permission to edit power levels in the child
const childPowerLevels = childRoom.currentState.getStateEvents(StateEvent.RoomPowerLevels, '');
const childPowerLevelsContent = childPowerLevels?.getContent() || {};
const childCreators = new Set<string>();
const creatorId = childRoom.currentState.getStateEvents('m.room.create', '')?.getContent()?.creator;
if (creatorId) childCreators.add(creatorId);
const myUserId = mx.getSafeUserId();
const myPower = childPowerLevelsContent.users?.[myUserId] ?? childPowerLevelsContent.users_default ?? 0;
// Check the specific power level required for m.room.power_levels events
const requiredPower = childPowerLevelsContent.events?.[StateEvent.RoomPowerLevels] ??
childPowerLevelsContent.state_default ?? 50;
if (myPower >= requiredPower || childCreators.has(myUserId)) {
syncable.push(childId);
} else {
nonSyncable.push({ roomId: childId, name: childRoom.name || childId });
}
});
return { syncableChildren: syncable, nonSyncableChildren: nonSyncable };
}, [allChildren, mx]);
const [syncState, syncPermissions] = useAsyncCallback(
useCallback(async () => {
// Get the power level tags from this space
const powerLevelTagsEvent = room.currentState.getStateEvents('in.cinny.room.power_level_tags', '');
const powerLevelTags = powerLevelTagsEvent?.getContent();
let completed = 0;
const total = syncableChildren.length;
for (const childId of syncableChildren) {
try {
// Copy the space's power levels to the child
await mx.sendStateEvent(childId, StateEvent.RoomPowerLevels as any, powerLevels);
// Also copy power level tags if they exist
if (powerLevelTags) {
await mx.sendStateEvent(childId, 'in.cinny.room.power_level_tags' as any, powerLevelTags);
}
completed += 1;
// Add a small delay to avoid rate limiting
if (completed < total) {
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
console.error(`Failed to sync permissions to ${childId}:`, error);
}
}
return { completed, total };
}, [mx, syncableChildren, powerLevels, room])
);
const handleEditPowers = () => {
setPowerEditor(true);
};
const handleSyncPermissions = () => {
syncPermissions();
};
if (canEditPowers && powerEditor) {
return <PowersEditor powerLevels={powerLevels} requestClose={() => setPowerEditor(false)} />;
}
const hasSyncableChildren = allChildren.length > 0;
const isSyncing = syncState.status === AsyncStatus.Loading;
const syncSuccess = syncState.status === AsyncStatus.Success;
const syncError = syncState.status === AsyncStatus.Error;
return (
<Page>
<PageHeader outlined={false}>
@@ -55,6 +157,65 @@ export function Permissions({ requestClose }: PermissionsProps) {
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
{canEditPermissions && hasSyncableChildren && (
<Box direction="Column" gap="100">
<Text size="L400">Sync Permissions</Text>
<Box direction="Column" gap="300" style={{ padding: 'var(--sp-300)' }}>
<Text size="T300">
Sync these permissions and power level tags to all child subspaces and rooms.
{nonSyncableChildren.length > 0 ? (
<> This will sync to {syncableChildren.length} child{' '}
{syncableChildren.length === 1 ? 'space/room' : 'spaces/rooms'} where you have permission.
<Text as="span" style={{ color: color.Warning.Main }}>
{' '}You don't have permission to edit {nonSyncableChildren.length} other{' '}
{nonSyncableChildren.length === 1 ? 'space/room' : 'spaces/rooms'}
{nonSyncableChildren.length <= 5 && (
<>: {nonSyncableChildren.map(c => c.name).join(', ')}</>
)}.
</Text>
</>
) : (
<> This will overwrite the power levels and custom role tags in {syncableChildren.length} child{' '}
{syncableChildren.length === 1 ? 'space/room' : 'spaces/rooms'}.
</>
)}
</Text>
<Box gap="200" alignItems="Center">
<Button
variant="Critical"
onClick={handleSyncPermissions}
disabled={isSyncing}
before={
isSyncing ? (
<Spinner variant="Critical" fill="Solid" size="50" />
) : (
<Icon src={Icons.Download} size="50" />
)
}
>
<Text size="B400">
{isSyncing
? 'Syncing...'
: `Sync to ${syncableChildren.length} ${
syncableChildren.length === 1 ? 'Child' : 'Children'
}${nonSyncableChildren.length > 0 ? ` (${nonSyncableChildren.length} skipped)` : ''}`}
</Text>
</Button>
{syncSuccess && (
<Text size="T300" style={{ color: color.Success.Main }}>
Successfully synced permissions to {syncState.data.completed} of{' '}
{syncState.data.total} children
</Text>
)}
{syncError && (
<Text size="T300" style={{ color: color.Critical.Main }}>
Error syncing permissions: {syncState.error.message}
</Text>
)}
</Box>
</Box>
</Box>
)}
<Powers
powerLevels={powerLevels}
onEdit={canEditPowers ? handleEditPowers : undefined}