feat: enhance update notification layout and add room info display in developer tools
This commit is contained in:
@@ -616,8 +616,10 @@ export function TitleBar({ mx, children }: TitleBarProps) {
|
|||||||
))}
|
))}
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
<UpdateNotification />
|
<Box alignItems="Center" gap="100" style={{ height: '100%', flexShrink: 0 }}>
|
||||||
<WindowControls />
|
<UpdateNotification />
|
||||||
|
<WindowControls />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ export const CheckButton = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: config.space.S200,
|
padding: 0,
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: color.Secondary.Main,
|
color: color.Secondary.Main,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
transition: 'background-color 0.15s',
|
transition: 'background-color 0.15s',
|
||||||
minWidth: toRem(32),
|
height: '32px',
|
||||||
minHeight: toRem(32),
|
width: '32px',
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
flexShrink: 0,
|
||||||
|
|
||||||
':hover': {
|
':hover': {
|
||||||
backgroundColor: color.Surface.ContainerHover,
|
backgroundColor: color.Surface.ContainerHover,
|
||||||
@@ -36,14 +38,16 @@ export const UpdateButton = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: config.space.S200,
|
padding: 0,
|
||||||
borderRadius: config.radii.R300,
|
borderRadius: config.radii.R300,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: color.Success.Main,
|
color: color.Success.Main,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
transition: 'background-color 0.15s',
|
transition: 'background-color 0.15s',
|
||||||
minWidth: toRem(32),
|
height: '32px',
|
||||||
minHeight: toRem(32),
|
width: '32px',
|
||||||
|
WebkitAppRegion: 'no-drag',
|
||||||
|
flexShrink: 0,
|
||||||
|
|
||||||
':hover': {
|
':hover': {
|
||||||
backgroundColor: color.Surface.ContainerHover,
|
backgroundColor: color.Surface.ContainerHover,
|
||||||
|
|||||||
@@ -118,42 +118,38 @@ export function UpdateNotification() {
|
|||||||
// Show check button if no update status
|
// Show check button if no update status
|
||||||
if (!updateAvailable && !updateReady && !checking) {
|
if (!updateAvailable && !updateReady && !checking) {
|
||||||
return (
|
return (
|
||||||
<Box gap="100" alignItems="Center">
|
<button
|
||||||
<button
|
className={css.CheckButton}
|
||||||
className={css.CheckButton}
|
onClick={handleCheckForUpdates}
|
||||||
onClick={handleCheckForUpdates}
|
aria-label="Check for updates"
|
||||||
aria-label="Check for updates"
|
title="Check for updates"
|
||||||
title="Check for updates"
|
type="button"
|
||||||
type="button"
|
>
|
||||||
>
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<path
|
||||||
<path
|
d="M8 2V10M8 10L5 7M8 10L11 7"
|
||||||
d="M8 2V10M8 10L5 7M8 10L11 7"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
strokeWidth="1.5"
|
||||||
strokeWidth="1.5"
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
strokeLinejoin="round"
|
||||||
strokeLinejoin="round"
|
/>
|
||||||
/>
|
<path
|
||||||
<path
|
d="M3 14H13"
|
||||||
d="M3 14H13"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
strokeWidth="1.5"
|
||||||
strokeWidth="1.5"
|
strokeLinecap="round"
|
||||||
strokeLinecap="round"
|
/>
|
||||||
/>
|
</svg>
|
||||||
</svg>
|
</button>
|
||||||
</button>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show checking state
|
// Show checking state
|
||||||
if (checking) {
|
if (checking) {
|
||||||
return (
|
return (
|
||||||
<Box gap="100" alignItems="Center">
|
<button className={css.UpdateButton} disabled type="button">
|
||||||
<button className={css.UpdateButton} disabled type="button">
|
<Spinner variant="Secondary" size="50" />
|
||||||
<Spinner variant="Secondary" size="50" />
|
</button>
|
||||||
</button>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +159,7 @@ export function UpdateNotification() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box gap="100" alignItems="Center">
|
<>
|
||||||
<button
|
<button
|
||||||
className={css.UpdateButton}
|
className={css.UpdateButton}
|
||||||
onClick={handleMenuToggle}
|
onClick={handleMenuToggle}
|
||||||
@@ -172,14 +168,7 @@ export function UpdateNotification() {
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{downloading ? (
|
{downloading ? (
|
||||||
<Box alignItems="Center" gap="100">
|
<Spinner variant="Secondary" size="50" />
|
||||||
<Spinner variant="Secondary" size="50" />
|
|
||||||
{downloadProgress > 0 && (
|
|
||||||
<Text size="T200" className={css.ProgressText}>
|
|
||||||
{downloadProgress}%
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
) : (
|
) : (
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<path
|
<path
|
||||||
@@ -246,6 +235,6 @@ export function UpdateNotification() {
|
|||||||
>
|
>
|
||||||
{null}
|
{null}
|
||||||
</PopOut>
|
</PopOut>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
@@ -11,7 +11,14 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
config,
|
config,
|
||||||
color,
|
color,
|
||||||
|
TextArea,
|
||||||
|
Dialog,
|
||||||
|
Overlay,
|
||||||
|
OverlayBackdrop,
|
||||||
|
OverlayCenter,
|
||||||
|
Modal,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SequenceCardStyle } from '../styles.css';
|
||||||
@@ -30,6 +37,10 @@ import {
|
|||||||
AccountDataSubmitCallback,
|
AccountDataSubmitCallback,
|
||||||
} from '../../../components/AccountDataEditor';
|
} from '../../../components/AccountDataEditor';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||||
|
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||||
|
import { isSpace } from '../../../utils/room';
|
||||||
|
|
||||||
type DeveloperToolsProps = {
|
type DeveloperToolsProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
@@ -41,6 +52,8 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
|||||||
|
|
||||||
const roomState = useRoomState(room);
|
const roomState = useRoomState(room);
|
||||||
const accountData = useRoomAccountData(room);
|
const accountData = useRoomAccountData(room);
|
||||||
|
const powerLevels = usePowerLevels(room);
|
||||||
|
const creators = useRoomCreators(room);
|
||||||
|
|
||||||
const [expandState, setExpandState] = useState(false);
|
const [expandState, setExpandState] = useState(false);
|
||||||
const [expandStateType, setExpandStateType] = useState<string>();
|
const [expandStateType, setExpandStateType] = useState<string>();
|
||||||
@@ -49,6 +62,72 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
|||||||
|
|
||||||
const [expandAccountData, setExpandAccountData] = useState(false);
|
const [expandAccountData, setExpandAccountData] = useState(false);
|
||||||
const [accountDataType, setAccountDataType] = useState<string | null>();
|
const [accountDataType, setAccountDataType] = useState<string | null>();
|
||||||
|
const [showRoomInfo, setShowRoomInfo] = useState(false);
|
||||||
|
|
||||||
|
const roomInfoJson = useMemo(() => {
|
||||||
|
const liveTimeline = room.getLiveTimeline();
|
||||||
|
const liveEvents = liveTimeline.getEvents();
|
||||||
|
const oldestEvent = liveEvents.length > 0 ? liveEvents[0] : null;
|
||||||
|
const newestEvent = liveEvents.length > 0 ? liveEvents[liveEvents.length - 1] : null;
|
||||||
|
|
||||||
|
// Get power level tags
|
||||||
|
const powerLevelTagsEvent = room.currentState.getStateEvents('in.cinny.room.power_level_tags', '');
|
||||||
|
const powerLevelTags = powerLevelTagsEvent?.getContent();
|
||||||
|
|
||||||
|
const info: any = {
|
||||||
|
roomId: room.roomId,
|
||||||
|
name: room.name,
|
||||||
|
canonicalAlias: room.canonicalAlias,
|
||||||
|
altAliases: room.getAltAliases(),
|
||||||
|
isSpace: isSpace(room),
|
||||||
|
joinRule: room.getJoinRule(),
|
||||||
|
guestAccess: room.getGuestAccess(),
|
||||||
|
historyVisibility: room.getHistoryVisibility(),
|
||||||
|
myUserId: mx.getUserId(),
|
||||||
|
myMembership: room.getMyMembership(),
|
||||||
|
creator: Array.from(creators),
|
||||||
|
memberCount: room.getJoinedMemberCount(),
|
||||||
|
invitedMemberCount: room.getInvitedMemberCount(),
|
||||||
|
powerLevels,
|
||||||
|
powerLevelTags: powerLevelTags || null,
|
||||||
|
tags: room.tags,
|
||||||
|
notificationCounts: {
|
||||||
|
total: room.getRoomUnreadNotificationCount('total'),
|
||||||
|
highlight: room.getRoomUnreadNotificationCount('highlight'),
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
oldestEventTs: oldestEvent?.getTs(),
|
||||||
|
newestEventTs: newestEvent?.getTs(),
|
||||||
|
liveEventsCount: liveEvents.length,
|
||||||
|
timelineSetSize: room.getTimelineSets().length,
|
||||||
|
},
|
||||||
|
encryption: room.hasEncryptionStateEvent(),
|
||||||
|
state: {} as any,
|
||||||
|
accountData: {} as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add all state events
|
||||||
|
roomState.forEach((stateKeyToEvents, eventType) => {
|
||||||
|
info.state[eventType] = {};
|
||||||
|
stateKeyToEvents.forEach((event, stateKey) => {
|
||||||
|
info.state[eventType][stateKey || '_default'] = {
|
||||||
|
content: event.getContent(),
|
||||||
|
sender: event.getSender(),
|
||||||
|
stateKey: event.getStateKey(),
|
||||||
|
eventId: event.getId(),
|
||||||
|
timestamp: event.getTs(),
|
||||||
|
unsigned: event.getUnsigned(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add account data
|
||||||
|
accountData.forEach((content, type) => {
|
||||||
|
info.accountData[type] = content;
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.stringify(info, null, 2);
|
||||||
|
}, [room, mx, roomState, accountData, powerLevels, creators]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setOpenStateEvent(undefined);
|
setOpenStateEvent(undefined);
|
||||||
@@ -82,6 +161,62 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
|||||||
return <StateEventEditor {...openStateEvent} requestClose={handleClose} />;
|
return <StateEventEditor {...openStateEvent} requestClose={handleClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showRoomInfo) {
|
||||||
|
return (
|
||||||
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
|
<OverlayCenter>
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setShowRoomInfo(false),
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Modal size="500" variant="Surface">
|
||||||
|
<Box direction="Column" gap="400" style={{ height: '80vh' }}>
|
||||||
|
<Box justifyContent="SpaceBetween" alignItems="Center" gap="200">
|
||||||
|
<Text size="H4" truncate>
|
||||||
|
{isSpace(room) ? 'Space' : 'Room'} Information
|
||||||
|
</Text>
|
||||||
|
<Box gap="200">
|
||||||
|
<Button
|
||||||
|
onClick={() => copyToClipboard(roomInfoJson)}
|
||||||
|
variant="Secondary"
|
||||||
|
fill="Soft"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Text size="B300">Copy All</Text>
|
||||||
|
</Button>
|
||||||
|
<IconButton onClick={() => setShowRoomInfo(false)} variant="Surface" size="300">
|
||||||
|
<Icon src={Icons.Cross} size="200" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box grow="Yes" style={{ minHeight: 0 }}>
|
||||||
|
<Scroll hideTrack visibility="Hover" size="300">
|
||||||
|
<TextArea
|
||||||
|
style={{
|
||||||
|
minHeight: '100%',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '12px',
|
||||||
|
resize: 'none',
|
||||||
|
}}
|
||||||
|
defaultValue={roomInfoJson}
|
||||||
|
readOnly
|
||||||
|
autoComplete="off"
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
|
/>
|
||||||
|
</Scroll>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</FocusTrap>
|
||||||
|
</OverlayCenter>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader outlined={false}>
|
<PageHeader outlined={false}>
|
||||||
@@ -144,6 +279,23 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<SettingTile
|
||||||
|
title={`View ${isSpace(room) ? 'Space' : 'Room'} Information`}
|
||||||
|
description="Display comprehensive information about this room/space including state, members, power levels, and more."
|
||||||
|
after={
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowRoomInfo(true)}
|
||||||
|
variant="Primary"
|
||||||
|
fill="Soft"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
outlined
|
||||||
|
before={<Icon src={Icons.Info} size="50" />}
|
||||||
|
>
|
||||||
|
<Text size="B300">View Info</Text>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
import { Box, Button, Icon, IconButton, Icons, Scroll, Spinner, Text, color } from 'folds';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||||
import { useRoom } from '../../../hooks/useRoom';
|
import { useRoom } from '../../../hooks/useRoom';
|
||||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||||
@@ -9,6 +10,10 @@ import { usePermissionGroups } from './usePermissionItems';
|
|||||||
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
|
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
|
||||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||||
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
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 = {
|
type PermissionsProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
@@ -18,6 +23,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
|
|||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
const powerLevels = usePowerLevels(room);
|
const powerLevels = usePowerLevels(room);
|
||||||
const creators = useRoomCreators(room);
|
const creators = useRoomCreators(room);
|
||||||
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||||
|
|
||||||
const permissions = useRoomPermissions(creators, powerLevels);
|
const permissions = useRoomPermissions(creators, powerLevels);
|
||||||
|
|
||||||
@@ -27,14 +33,110 @@ export function Permissions({ requestClose }: PermissionsProps) {
|
|||||||
|
|
||||||
const [powerEditor, setPowerEditor] = useState(false);
|
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 = () => {
|
const handleEditPowers = () => {
|
||||||
setPowerEditor(true);
|
setPowerEditor(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSyncPermissions = () => {
|
||||||
|
syncPermissions();
|
||||||
|
};
|
||||||
|
|
||||||
if (canEditPowers && powerEditor) {
|
if (canEditPowers && powerEditor) {
|
||||||
return <PowersEditor powerLevels={powerLevels} requestClose={() => setPowerEditor(false)} />;
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader outlined={false}>
|
<PageHeader outlined={false}>
|
||||||
@@ -55,6 +157,65 @@ export function Permissions({ requestClose }: PermissionsProps) {
|
|||||||
<Scroll hideTrack visibility="Hover">
|
<Scroll hideTrack visibility="Hover">
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<Box direction="Column" gap="700">
|
<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
|
<Powers
|
||||||
powerLevels={powerLevels}
|
powerLevels={powerLevels}
|
||||||
onEdit={canEditPowers ? handleEditPowers : undefined}
|
onEdit={canEditPowers ? handleEditPowers : undefined}
|
||||||
|
|||||||
Reference in New Issue
Block a user