multiuser

This commit is contained in:
2026-03-12 01:08:16 +11:00
parent 30a7725769
commit 7cf6b4e6ae
12 changed files with 939 additions and 247 deletions

View File

@@ -94,11 +94,79 @@ export function Permissions({ requestClose }: PermissionsProps) {
let completed = 0;
const total = syncableChildren.length;
const myUserId = mx.getSafeUserId();
for (const childId of syncableChildren) {
try {
// Copy the space's power levels to the child
await mx.sendStateEvent(childId, StateEvent.RoomPowerLevels as any, powerLevels);
const childRoom = mx.getRoom(childId);
if (!childRoom) continue;
// Get current power levels in the child room
const childPowerLevelsEvent = childRoom.currentState.getStateEvents(StateEvent.RoomPowerLevels, '');
const childPowerLevels = childPowerLevelsEvent?.getContent() || {};
// Get my power level in the child room
const myPowerInChild = childPowerLevels.users?.[myUserId] ?? childPowerLevels.users_default ?? 0;
// Build merged users: only sync users with power <= mine, preserve higher-powered users
const mergedUsers: Record<string, number> = {};
// Copy users from parent where power <= my power in child
if (powerLevels.users) {
Object.entries(powerLevels.users).forEach(([userId, power]) => {
if (power <= myPowerInChild) {
mergedUsers[userId] = power;
}
});
}
// Preserve all users in child room who have power >= mine
if (childPowerLevels.users) {
Object.entries(childPowerLevels.users).forEach(([userId, power]) => {
if (power >= myPowerInChild) {
mergedUsers[userId] = power;
}
});
}
// Cap all power level values at my power level
const capPowerLevel = (value: any) => {
if (typeof value === 'number') {
return Math.min(value, myPowerInChild);
}
return value;
};
// Create the merged power levels with capped values
const mergedPowerLevels: any = {
...powerLevels,
users: mergedUsers,
ban: capPowerLevel(powerLevels.ban),
kick: capPowerLevel(powerLevels.kick),
redact: capPowerLevel(powerLevels.redact),
invite: capPowerLevel(powerLevels.invite),
events_default: capPowerLevel(powerLevels.events_default),
state_default: capPowerLevel(powerLevels.state_default),
users_default: capPowerLevel(powerLevels.users_default),
};
// Cap event-specific power levels
if (powerLevels.events) {
mergedPowerLevels.events = {};
Object.entries(powerLevels.events).forEach(([eventType, power]) => {
mergedPowerLevels.events[eventType] = capPowerLevel(power);
});
}
// Cap notification power levels
if (powerLevels.notifications) {
mergedPowerLevels.notifications = {};
Object.entries(powerLevels.notifications).forEach(([key, power]) => {
mergedPowerLevels.notifications[key] = capPowerLevel(power);
});
}
await mx.sendStateEvent(childId, StateEvent.RoomPowerLevels as any, mergedPowerLevels);
// Also copy power level tags if they exist
if (powerLevelTags) {
@@ -120,6 +188,99 @@ export function Permissions({ requestClose }: PermissionsProps) {
}, [mx, syncableChildren, powerLevels, room])
);
const [syncPowerLevelsState, syncPowerLevelsOnly] = useAsyncCallback(
useCallback(async () => {
let completed = 0;
const total = syncableChildren.length;
const myUserId = mx.getSafeUserId();
for (const childId of syncableChildren) {
try {
const childRoom = mx.getRoom(childId);
if (!childRoom) continue;
// Get current power levels in the child room
const childPowerLevelsEvent = childRoom.currentState.getStateEvents(StateEvent.RoomPowerLevels, '');
const childPowerLevels = childPowerLevelsEvent?.getContent() || {};
// Get my power level in the child room
const myPowerInChild = childPowerLevels.users?.[myUserId] ?? childPowerLevels.users_default ?? 0;
// Build merged users: only sync users with power <= mine, preserve higher-powered users
const mergedUsers: Record<string, number> = {};
// Copy users from parent where power <= my power in child
if (powerLevels.users) {
Object.entries(powerLevels.users).forEach(([userId, power]) => {
if (power <= myPowerInChild) {
mergedUsers[userId] = power;
}
});
}
// Preserve all users in child room who have power >= mine
if (childPowerLevels.users) {
Object.entries(childPowerLevels.users).forEach(([userId, power]) => {
if (power >= myPowerInChild) {
mergedUsers[userId] = power;
}
});
}
// Cap all power level values at my power level
const capPowerLevel = (value: any) => {
if (typeof value === 'number') {
return Math.min(value, myPowerInChild);
}
return value;
};
// Create the merged power levels with capped values
const mergedPowerLevels: any = {
...powerLevels,
users: mergedUsers,
ban: capPowerLevel(powerLevels.ban),
kick: capPowerLevel(powerLevels.kick),
redact: capPowerLevel(powerLevels.redact),
invite: capPowerLevel(powerLevels.invite),
events_default: capPowerLevel(powerLevels.events_default),
state_default: capPowerLevel(powerLevels.state_default),
users_default: capPowerLevel(powerLevels.users_default),
};
// Cap event-specific power levels
if (powerLevels.events) {
mergedPowerLevels.events = {};
Object.entries(powerLevels.events).forEach(([eventType, power]) => {
mergedPowerLevels.events[eventType] = capPowerLevel(power);
});
}
// Cap notification power levels
if (powerLevels.notifications) {
mergedPowerLevels.notifications = {};
Object.entries(powerLevels.notifications).forEach(([key, power]) => {
mergedPowerLevels.notifications[key] = capPowerLevel(power);
});
}
await mx.sendStateEvent(childId, StateEvent.RoomPowerLevels as any, mergedPowerLevels);
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 power levels to ${childId}:`, error);
}
}
return { completed, total };
}, [mx, syncableChildren, powerLevels])
);
const handleEditPowers = () => {
setPowerEditor(true);
};
@@ -128,6 +289,10 @@ export function Permissions({ requestClose }: PermissionsProps) {
syncPermissions();
};
const handleSyncPowerLevelsOnly = () => {
syncPowerLevelsOnly();
};
if (canEditPowers && powerEditor) {
return <PowersEditor powerLevels={powerLevels} requestClose={() => setPowerEditor(false)} />;
}
@@ -136,6 +301,9 @@ export function Permissions({ requestClose }: PermissionsProps) {
const isSyncing = syncState.status === AsyncStatus.Loading;
const syncSuccess = syncState.status === AsyncStatus.Success;
const syncError = syncState.status === AsyncStatus.Error;
const isPowerLevelsSyncing = syncPowerLevelsState.status === AsyncStatus.Loading;
const powerLevelsSyncSuccess = syncPowerLevelsState.status === AsyncStatus.Success;
const powerLevelsSyncError = syncPowerLevelsState.status === AsyncStatus.Error;
return (
<Page>
@@ -157,7 +325,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
{canEditPermissions && hasSyncableChildren && (
{syncableChildren.length > 0 && (
<Box direction="Column" gap="100">
<Text size="L400">Sync Permissions</Text>
<Box direction="Column" gap="300" style={{ padding: 'var(--sp-300)' }}>
@@ -180,38 +348,73 @@ export function Permissions({ requestClose }: PermissionsProps) {
</>
)}
</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 direction="Column" gap="200">
<Box gap="200" alignItems="Center">
<Button
variant="Critical"
onClick={handleSyncPermissions}
disabled={isSyncing || isPowerLevelsSyncing}
before={
isSyncing ? (
<Spinner variant="Critical" fill="Solid" size="50" />
) : (
<Icon src={Icons.Download} size="50" />
)
}
>
<Text size="B400">
{isSyncing
? 'Syncing All...'
: `Sync All 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 gap="200" alignItems="Center">
<Button
variant="Secondary"
onClick={handleSyncPowerLevelsOnly}
disabled={isSyncing || isPowerLevelsSyncing}
before={
isPowerLevelsSyncing ? (
<Spinner variant="Secondary" fill="Solid" size="50" />
) : (
<Icon src={Icons.Download} size="50" />
)
}
>
<Text size="B400">
{isPowerLevelsSyncing
? 'Syncing Power Levels...'
: `Sync Power Levels Only to ${syncableChildren.length} ${
syncableChildren.length === 1 ? 'Child' : 'Children'
}${nonSyncableChildren.length > 0 ? ` (${nonSyncableChildren.length} skipped)` : ''}`}
</Text>
</Button>
{powerLevelsSyncSuccess && (
<Text size="T300" style={{ color: color.Success.Main }}>
Successfully synced power levels to {syncPowerLevelsState.data.completed} of{' '}
{syncPowerLevelsState.data.total} children
</Text>
)}
{powerLevelsSyncError && (
<Text size="T300" style={{ color: color.Critical.Main }}>
Error syncing power levels: {syncPowerLevelsState.error.message}
</Text>
)}
</Box>
</Box>
</Box>
</Box>