multiuser
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user