forked from github/cinny
New room settings, add customizable power levels and dev tools (#2222)
* WIP - add room settings dialog * join rule setting - WIP * show emojis & stickers in room settings - WIP * restyle join rule switcher * Merge branch 'dev' into new-room-settings * add join rule hook * open room settings from global state * open new room settings from all places * rearrange settings menu item * add option for creating new image pack * room devtools - WIP * render room state events as list * add option to open state event * add option to edit state event * refactor text area code editor into hook * add option to send message and state event * add cutout card component * add hook for room account data * display room account data - WIP * refactor global account data editor component * add account data editor in room * fix font style in devtool * show state events in compact form * add option to delete room image pack * add server badge component * add member tile component * render members in room settings * add search in room settings member * add option to reset member search * add filter in room members * fix member virtual item key * remove color from serve badge in room members * show room in settings * fix loading indicator position * power level tags in room setting - WIP * generate fallback tag in backward compatible way * add color picker * add powers editor - WIP * add props to stop adding emoji to recent usage * add beta feature notice badge * add types for power level tag icon * refactor image pack rooms code to hook * option for adding new power levels tags * remove console log * refactor power icon * add option to edit power level tags * remove power level from powers pill * fix power level labels * add option to delete power levels * fix long power level name shrinks power integer * room permissions - WIP * add power level selector component * add room permissions * move user default permission setting to other group * add power permission peek menu * fix weigh of power switch text * hide above for max power in permission switcher * improve beta badge description * render room profile in room settings * add option to edit room profile * make room topic input text area * add option to enable room encryption in room settings * add option to change message history visibility * add option to change join rule * add option for addresses in room settings * close encryption dialog after enabling
This commit is contained in:
287
src/app/features/room-settings/permissions/PermissionGroups.tsx
Normal file
287
src/app/features/room-settings/permissions/PermissionGroups.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Badge, Box, Button, Chip, config, Icon, Icons, Menu, Spinner, Text } from 'folds';
|
||||
import produce from 'immer';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import {
|
||||
applyPermissionPower,
|
||||
getPermissionPower,
|
||||
IPowerLevels,
|
||||
PermissionLocation,
|
||||
usePowerLevelsAPI,
|
||||
} from '../../../hooks/usePowerLevels';
|
||||
import { usePermissionGroups } from './usePermissionItems';
|
||||
import { getPowers, usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { PowerSwitcher } from '../../../components/power';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
|
||||
const USER_DEFAULT_LOCATION: PermissionLocation = {
|
||||
user: true,
|
||||
};
|
||||
|
||||
type PermissionGroupsProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
};
|
||||
export function PermissionGroups({ powerLevels }: PermissionGroupsProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const alive = useAlive();
|
||||
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
|
||||
const canChangePermission = canSendStateEvent(
|
||||
StateEvent.RoomPowerLevels,
|
||||
getPowerLevel(mx.getSafeUserId())
|
||||
);
|
||||
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const maxPower = useMemo(() => Math.max(...getPowers(powerLevelTags)), [powerLevelTags]);
|
||||
|
||||
const permissionGroups = usePermissionGroups();
|
||||
|
||||
const [permissionUpdate, setPermissionUpdate] = useState<Map<PermissionLocation, number>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// reset permission update if component rerender
|
||||
// as permission location object reference has changed
|
||||
setPermissionUpdate(new Map());
|
||||
}, [permissionGroups]);
|
||||
|
||||
const handleChangePermission = (
|
||||
location: PermissionLocation,
|
||||
newPower: number,
|
||||
currentPower: number
|
||||
) => {
|
||||
setPermissionUpdate((p) => {
|
||||
const up: typeof p = new Map();
|
||||
p.forEach((value, key) => {
|
||||
up.set(key, value);
|
||||
});
|
||||
if (newPower === currentPower) {
|
||||
up.delete(location);
|
||||
} else {
|
||||
up.set(location, newPower);
|
||||
}
|
||||
return up;
|
||||
});
|
||||
};
|
||||
|
||||
const [applyState, applyChanges] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
const editedPowerLevels = produce(powerLevels, (draftPowerLevels) => {
|
||||
permissionGroups.forEach((group) =>
|
||||
group.items.forEach((item) => {
|
||||
const power = getPermissionPower(powerLevels, item.location);
|
||||
applyPermissionPower(draftPowerLevels, item.location, power);
|
||||
})
|
||||
);
|
||||
permissionUpdate.forEach((power, location) =>
|
||||
applyPermissionPower(draftPowerLevels, location, power)
|
||||
);
|
||||
return draftPowerLevels;
|
||||
});
|
||||
await mx.sendStateEvent(room.roomId, StateEvent.RoomPowerLevels as any, editedPowerLevels);
|
||||
}, [mx, room, powerLevels, permissionUpdate, permissionGroups])
|
||||
);
|
||||
|
||||
const resetChanges = useCallback(() => {
|
||||
setPermissionUpdate(new Map());
|
||||
}, []);
|
||||
|
||||
const handleApplyChanges = () => {
|
||||
applyChanges().then(() => {
|
||||
if (alive()) {
|
||||
resetChanges();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const applyingChanges = applyState.status === AsyncStatus.Loading;
|
||||
const hasChanges = permissionUpdate.size > 0;
|
||||
|
||||
const renderUserGroup = () => {
|
||||
const power = getPermissionPower(powerLevels, USER_DEFAULT_LOCATION);
|
||||
const powerUpdate = permissionUpdate.get(USER_DEFAULT_LOCATION);
|
||||
const value = powerUpdate ?? power;
|
||||
|
||||
const tag = getPowerLevelTag(value);
|
||||
const powerChanges = value !== power;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Users</Text>
|
||||
<SequenceCard
|
||||
variant="SurfaceVariant"
|
||||
className={SequenceCardStyle}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Default Power"
|
||||
description="Default power level for all users."
|
||||
after={
|
||||
<PowerSwitcher
|
||||
powerLevelTags={powerLevelTags}
|
||||
value={value}
|
||||
onChange={(v) => handleChangePermission(USER_DEFAULT_LOCATION, v, power)}
|
||||
>
|
||||
{(handleOpen, opened) => (
|
||||
<Chip
|
||||
variant={powerChanges ? 'Success' : 'Secondary'}
|
||||
outlined={powerChanges}
|
||||
fill="Soft"
|
||||
radii="Pill"
|
||||
aria-selected={opened}
|
||||
disabled={!canChangePermission || applyingChanges}
|
||||
after={
|
||||
powerChanges && (
|
||||
<Badge size="200" variant="Success" fill="Solid" radii="Pill" />
|
||||
)
|
||||
}
|
||||
before={
|
||||
canChangePermission && (
|
||||
<Icon size="50" src={opened ? Icons.ChevronTop : Icons.ChevronBottom} />
|
||||
)
|
||||
}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
{tag.name}
|
||||
</Text>
|
||||
</Chip>
|
||||
)}
|
||||
</PowerSwitcher>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderUserGroup()}
|
||||
{permissionGroups.map((group, groupIndex) => (
|
||||
<Box key={groupIndex} direction="Column" gap="100">
|
||||
<Text size="L400">{group.name}</Text>
|
||||
{group.items.map((item, itemIndex) => {
|
||||
const power = getPermissionPower(powerLevels, item.location);
|
||||
const powerUpdate = permissionUpdate.get(item.location);
|
||||
const value = powerUpdate ?? power;
|
||||
|
||||
const tag = getPowerLevelTag(value);
|
||||
const powerChanges = value !== power;
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
key={itemIndex}
|
||||
variant="SurfaceVariant"
|
||||
className={SequenceCardStyle}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title={item.name}
|
||||
description={item.description}
|
||||
after={
|
||||
<PowerSwitcher
|
||||
powerLevelTags={powerLevelTags}
|
||||
value={value}
|
||||
onChange={(v) => handleChangePermission(item.location, v, power)}
|
||||
>
|
||||
{(handleOpen, opened) => (
|
||||
<Chip
|
||||
variant={powerChanges ? 'Success' : 'Secondary'}
|
||||
outlined={powerChanges}
|
||||
fill="Soft"
|
||||
radii="Pill"
|
||||
aria-selected={opened}
|
||||
disabled={!canChangePermission || applyingChanges}
|
||||
after={
|
||||
powerChanges && (
|
||||
<Badge size="200" variant="Success" fill="Solid" radii="Pill" />
|
||||
)
|
||||
}
|
||||
before={
|
||||
canChangePermission && (
|
||||
<Icon
|
||||
size="50"
|
||||
src={opened ? Icons.ChevronTop : Icons.ChevronBottom}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
{tag.name}
|
||||
</Text>
|
||||
{value < maxPower && <Text size="T200">& Above</Text>}
|
||||
</Chip>
|
||||
)}
|
||||
</PowerSwitcher>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{hasChanges && (
|
||||
<Menu
|
||||
style={{
|
||||
position: 'sticky',
|
||||
padding: config.space.S200,
|
||||
paddingLeft: config.space.S400,
|
||||
bottom: config.space.S400,
|
||||
left: config.space.S400,
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
variant="Success"
|
||||
>
|
||||
<Box alignItems="Center" gap="400">
|
||||
<Box grow="Yes" direction="Column">
|
||||
{applyState.status === AsyncStatus.Error ? (
|
||||
<Text size="T200">
|
||||
<b>Failed to apply changes! Please try again.</b>
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="T200">
|
||||
<b>Changes saved! Apply when ready.</b>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box shrink="No" gap="200">
|
||||
<Button
|
||||
size="300"
|
||||
variant="Success"
|
||||
fill="None"
|
||||
radii="300"
|
||||
disabled={applyingChanges}
|
||||
onClick={resetChanges}
|
||||
>
|
||||
<Text size="B300">Reset</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Success"
|
||||
radii="300"
|
||||
disabled={applyingChanges}
|
||||
before={applyingChanges && <Spinner variant="Success" fill="Solid" size="100" />}
|
||||
onClick={handleApplyChanges}
|
||||
>
|
||||
<Text size="B300">Apply Changes</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
66
src/app/features/room-settings/permissions/Permissions.tsx
Normal file
66
src/app/features/room-settings/permissions/Permissions.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { Powers } from './Powers';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { PowersEditor } from './PowersEditor';
|
||||
import { PermissionGroups } from './PermissionGroups';
|
||||
|
||||
type PermissionsProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function Permissions({ requestClose }: PermissionsProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
|
||||
const canEditPowers = canSendStateEvent(
|
||||
StateEvent.PowerLevelTags,
|
||||
getPowerLevel(mx.getSafeUserId())
|
||||
);
|
||||
|
||||
const [powerEditor, setPowerEditor] = useState(false);
|
||||
|
||||
const handleEditPowers = () => {
|
||||
setPowerEditor(true);
|
||||
};
|
||||
|
||||
if (canEditPowers && powerEditor) {
|
||||
return <PowersEditor powerLevels={powerLevels} requestClose={() => setPowerEditor(false)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
Permissions
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="Surface">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
<Powers
|
||||
powerLevels={powerLevels}
|
||||
onEdit={canEditPowers ? handleEditPowers : undefined}
|
||||
/>
|
||||
<PermissionGroups powerLevels={powerLevels} />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
170
src/app/features/room-settings/permissions/Powers.tsx
Normal file
170
src/app/features/room-settings/permissions/Powers.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useState, MouseEventHandler, ReactNode } from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Text,
|
||||
RectCords,
|
||||
PopOut,
|
||||
Menu,
|
||||
Scroll,
|
||||
toRem,
|
||||
config,
|
||||
color,
|
||||
} from 'folds';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { getPowers, getTagIconSrc, usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { getPermissionPower, IPowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { PowerColorBadge, PowerIcon } from '../../../components/power';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { usePermissionGroups } from './usePermissionItems';
|
||||
|
||||
type PeekPermissionsProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
power: number;
|
||||
children: (handleOpen: MouseEventHandler<HTMLButtonElement>, opened: boolean) => ReactNode;
|
||||
};
|
||||
function PeekPermissions({ powerLevels, power, children }: PeekPermissionsProps) {
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const permissionGroups = usePermissionGroups();
|
||||
|
||||
const handleOpen: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
return (
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
offset={5}
|
||||
position="Bottom"
|
||||
align="Center"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setMenuCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
style={{
|
||||
maxHeight: '75vh',
|
||||
maxWidth: toRem(300),
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Box grow="Yes" tabIndex={0}>
|
||||
<Scroll size="0" hideTrack visibility="Hover">
|
||||
<Box style={{ padding: config.space.S200 }} direction="Column" gap="400">
|
||||
{permissionGroups.map((group, groupIndex) => (
|
||||
<Box key={groupIndex} direction="Column" gap="100">
|
||||
<Text size="L400">{group.name}</Text>
|
||||
<div>
|
||||
{group.items.map((item, itemIndex) => {
|
||||
const requiredPower = getPermissionPower(powerLevels, item.location);
|
||||
const hasPower = requiredPower <= power;
|
||||
|
||||
return (
|
||||
<Text
|
||||
key={itemIndex}
|
||||
size="T200"
|
||||
style={{
|
||||
color: hasPower ? undefined : color.Critical.Main,
|
||||
}}
|
||||
>
|
||||
{hasPower ? '✅' : '❌'} {item.name}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
{children(handleOpen, !!menuCords)}
|
||||
</PopOut>
|
||||
);
|
||||
}
|
||||
|
||||
type PowersProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
onEdit?: () => void;
|
||||
};
|
||||
export function Powers({ powerLevels, onEdit }: PowersProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const room = useRoom();
|
||||
const [powerLevelTags] = usePowerLevelTags(room, powerLevels);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<SequenceCard
|
||||
variant="SurfaceVariant"
|
||||
className={SequenceCardStyle}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="Power Levels"
|
||||
description="Manage and customize incremental power levels for users."
|
||||
after={
|
||||
onEdit && (
|
||||
<Box gap="200">
|
||||
<Button
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Text size="B300">Edit</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SettingTile>
|
||||
<Box gap="200" wrap="Wrap">
|
||||
{getPowers(powerLevelTags).map((power) => {
|
||||
const tag = powerLevelTags[power];
|
||||
const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon);
|
||||
|
||||
return (
|
||||
<PeekPermissions key={power} powerLevels={powerLevels} power={power}>
|
||||
{(openMenu, opened) => (
|
||||
<Chip
|
||||
onClick={openMenu}
|
||||
variant="Secondary"
|
||||
aria-pressed={opened}
|
||||
radii="300"
|
||||
before={<PowerColorBadge color={tag.color} />}
|
||||
after={tagIconSrc && <PowerIcon size="50" iconSrc={tagIconSrc} />}
|
||||
>
|
||||
<Text size="T300" truncate>
|
||||
<b>{tag.name}</b>
|
||||
</Text>
|
||||
</Chip>
|
||||
)}
|
||||
</PeekPermissions>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</SettingTile>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
579
src/app/features/room-settings/permissions/PowersEditor.tsx
Normal file
579
src/app/features/room-settings/permissions/PowersEditor.tsx
Normal file
@@ -0,0 +1,579 @@
|
||||
import React, { FormEventHandler, MouseEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Chip,
|
||||
Icon,
|
||||
Icons,
|
||||
IconButton,
|
||||
Scroll,
|
||||
Button,
|
||||
Input,
|
||||
RectCords,
|
||||
PopOut,
|
||||
Menu,
|
||||
config,
|
||||
Spinner,
|
||||
toRem,
|
||||
TooltipProvider,
|
||||
Tooltip,
|
||||
} from 'folds';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { IPowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import {
|
||||
getPowers,
|
||||
getTagIconSrc,
|
||||
getUsedPowers,
|
||||
PowerLevelTag,
|
||||
PowerLevelTagIcon,
|
||||
PowerLevelTags,
|
||||
usePowerLevelTags,
|
||||
} from '../../../hooks/usePowerLevelTags';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
|
||||
import { PowerColorBadge, PowerIcon } from '../../../components/power';
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
import { EmojiBoard } from '../../../components/emoji-board';
|
||||
import { useImagePackRooms } from '../../../hooks/useImagePackRooms';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useFilePicker } from '../../../hooks/useFilePicker';
|
||||
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
||||
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
import { BetaNoticeBadge } from '../../../components/BetaNoticeBadge';
|
||||
|
||||
type EditPowerProps = {
|
||||
maxPower: number;
|
||||
power?: number;
|
||||
tag?: PowerLevelTag;
|
||||
onSave: (power: number, tag: PowerLevelTag) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
||||
const imagePackRooms = useImagePackRooms(room.roomId, roomToParents);
|
||||
|
||||
const [iconFile, setIconFile] = useState<File>();
|
||||
const pickFile = useFilePicker(setIconFile, false);
|
||||
|
||||
const [tagColor, setTagColor] = useState<string | undefined>(tag?.color);
|
||||
const [tagIcon, setTagIcon] = useState<PowerLevelTagIcon | undefined>(tag?.icon);
|
||||
const uploadingIcon = iconFile && !tagIcon;
|
||||
const tagIconSrc = tagIcon && getTagIconSrc(mx, useAuthentication, tagIcon);
|
||||
|
||||
const iconUploadAtom = useMemo(() => {
|
||||
if (iconFile) return createUploadAtom(iconFile);
|
||||
return undefined;
|
||||
}, [iconFile]);
|
||||
|
||||
const handleRemoveIconUpload = useCallback(() => {
|
||||
setIconFile(undefined);
|
||||
}, []);
|
||||
|
||||
const handleIconUploaded = useCallback((upload: UploadSuccess) => {
|
||||
setTagIcon({
|
||||
key: upload.mxc,
|
||||
});
|
||||
setIconFile(undefined);
|
||||
}, []);
|
||||
|
||||
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (uploadingIcon) return;
|
||||
|
||||
const target = evt.target as HTMLFormElement | undefined;
|
||||
const powerInput = target?.powerInput as HTMLInputElement | undefined;
|
||||
const nameInput = target?.nameInput as HTMLInputElement | undefined;
|
||||
if (!powerInput || !nameInput) return;
|
||||
|
||||
const tagPower = parseInt(powerInput.value, 10);
|
||||
if (Number.isNaN(tagPower)) return;
|
||||
if (tagPower > maxPower) return;
|
||||
const tagName = nameInput.value.trim();
|
||||
if (!tagName) return;
|
||||
|
||||
const editedTag: PowerLevelTag = {
|
||||
name: tagName,
|
||||
color: tagColor,
|
||||
icon: tagIcon,
|
||||
};
|
||||
|
||||
onSave(power ?? tagPower, editedTag);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box onSubmit={handleSubmit} as="form" direction="Column" gap="400">
|
||||
<Box direction="Column" gap="300">
|
||||
<Box gap="200">
|
||||
<Box shrink="No" direction="Column" gap="100">
|
||||
<Text size="L400">Color</Text>
|
||||
<Box gap="200">
|
||||
<HexColorPickerPopOut
|
||||
picker={<HexColorPicker color={tagColor} onChange={setTagColor} />}
|
||||
onRemove={() => setTagColor(undefined)}
|
||||
>
|
||||
{(openPicker, opened) => (
|
||||
<Button
|
||||
aria-pressed={opened}
|
||||
onClick={openPicker}
|
||||
size="300"
|
||||
type="button"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
before={<PowerColorBadge color={tagColor} />}
|
||||
>
|
||||
<Text size="B300">Pick</Text>
|
||||
</Button>
|
||||
)}
|
||||
</HexColorPickerPopOut>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Text size="L400">Name</Text>
|
||||
<Input
|
||||
name="nameInput"
|
||||
defaultValue={tag?.name}
|
||||
placeholder="Bot"
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
<Box style={{ maxWidth: toRem(74) }} grow="Yes" direction="Column" gap="100">
|
||||
<Text size="L400">Power</Text>
|
||||
<Input
|
||||
defaultValue={power}
|
||||
name="powerInput"
|
||||
size="300"
|
||||
variant={typeof power === 'number' ? 'SurfaceVariant' : 'Secondary'}
|
||||
radii="300"
|
||||
type="number"
|
||||
placeholder="75"
|
||||
max={maxPower}
|
||||
outlined={typeof power === 'number'}
|
||||
readOnly={typeof power === 'number'}
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Icon</Text>
|
||||
{iconUploadAtom && !tagIconSrc ? (
|
||||
<CompactUploadCardRenderer
|
||||
uploadAtom={iconUploadAtom}
|
||||
onRemove={handleRemoveIconUpload}
|
||||
onComplete={handleIconUploaded}
|
||||
/>
|
||||
) : (
|
||||
<Box gap="200" alignItems="Center">
|
||||
{tagIconSrc ? (
|
||||
<>
|
||||
<PowerIcon size="500" iconSrc={tagIconSrc} />
|
||||
<Button
|
||||
onClick={() => setTagIcon(undefined)}
|
||||
type="button"
|
||||
size="300"
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
radii="300"
|
||||
>
|
||||
<Text size="B300">Remove</Text>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UseStateProvider initial={undefined}>
|
||||
{(cords: RectCords | undefined, setCords) => (
|
||||
<PopOut
|
||||
position="Bottom"
|
||||
anchor={cords}
|
||||
content={
|
||||
<EmojiBoard
|
||||
imagePackRooms={imagePackRooms}
|
||||
returnFocusOnDeactivate={false}
|
||||
allowTextCustomEmoji={false}
|
||||
addToRecentEmoji={false}
|
||||
onEmojiSelect={(key) => {
|
||||
setTagIcon({ key });
|
||||
setCords(undefined);
|
||||
}}
|
||||
onCustomEmojiSelect={(mxc) => {
|
||||
setTagIcon({ key: mxc });
|
||||
setCords(undefined);
|
||||
}}
|
||||
requestClose={() => {
|
||||
setCords(undefined);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={
|
||||
((evt) =>
|
||||
setCords(
|
||||
evt.currentTarget.getBoundingClientRect()
|
||||
)) as MouseEventHandler<HTMLButtonElement>
|
||||
}
|
||||
type="button"
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
before={<Icon size="50" src={Icons.SmilePlus} />}
|
||||
>
|
||||
<Text size="B300">Pick</Text>
|
||||
</Button>
|
||||
</PopOut>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
<Button
|
||||
onClick={() => pickFile('image/*')}
|
||||
type="button"
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="None"
|
||||
radii="300"
|
||||
>
|
||||
<Text size="B300">Import</Text>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box direction="Row" gap="200" justifyContent="Start">
|
||||
<Button
|
||||
style={{ minWidth: toRem(64) }}
|
||||
type="submit"
|
||||
size="300"
|
||||
variant="Success"
|
||||
radii="300"
|
||||
disabled={uploadingIcon}
|
||||
>
|
||||
<Text size="B300">Save</Text>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="300"
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Text size="B300">Cancel</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type PowersEditorProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function PowersEditor({ powerLevels, requestClose }: PowersEditorProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const room = useRoom();
|
||||
const alive = useAlive();
|
||||
const [usedPowers, maxPower] = useMemo(() => {
|
||||
const up = getUsedPowers(powerLevels);
|
||||
return [up, Math.max(...Array.from(up))];
|
||||
}, [powerLevels]);
|
||||
|
||||
const [powerLevelTags] = usePowerLevelTags(room, powerLevels);
|
||||
const [editedPowerTags, setEditedPowerTags] = useState<PowerLevelTags>();
|
||||
const [deleted, setDeleted] = useState<Set<number>>(new Set());
|
||||
|
||||
const [createTag, setCreateTag] = useState(false);
|
||||
|
||||
const handleToggleDelete = useCallback((power: number) => {
|
||||
setDeleted((powers) => {
|
||||
const newIds = new Set(powers);
|
||||
if (newIds.has(power)) {
|
||||
newIds.delete(power);
|
||||
} else {
|
||||
newIds.add(power);
|
||||
}
|
||||
return newIds;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSaveTag = useCallback(
|
||||
(power: number, tag: PowerLevelTag) => {
|
||||
setEditedPowerTags((tags) => {
|
||||
const editedTags = { ...(tags ?? powerLevelTags) };
|
||||
editedTags[power] = tag;
|
||||
return editedTags;
|
||||
});
|
||||
},
|
||||
[powerLevelTags]
|
||||
);
|
||||
|
||||
const [applyState, applyChanges] = useAsyncCallback(
|
||||
useCallback(async () => {
|
||||
const content: PowerLevelTags = { ...(editedPowerTags ?? powerLevelTags) };
|
||||
deleted.forEach((power) => {
|
||||
delete content[power];
|
||||
});
|
||||
await mx.sendStateEvent(room.roomId, StateEvent.PowerLevelTags as any, content);
|
||||
}, [mx, room, powerLevelTags, editedPowerTags, deleted])
|
||||
);
|
||||
|
||||
const resetChanges = useCallback(() => {
|
||||
setEditedPowerTags(undefined);
|
||||
setDeleted(new Set());
|
||||
}, []);
|
||||
|
||||
const handleApplyChanges = () => {
|
||||
applyChanges().then(() => {
|
||||
if (alive()) {
|
||||
resetChanges();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const applyingChanges = applyState.status === AsyncStatus.Loading;
|
||||
const hasChanges = editedPowerTags || deleted.size > 0;
|
||||
|
||||
const powerTags = editedPowerTags ?? powerLevelTags;
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false} balance>
|
||||
<Box alignItems="Center" grow="Yes" gap="200">
|
||||
<Box alignItems="Inherit" grow="Yes" gap="200">
|
||||
<Chip
|
||||
size="500"
|
||||
radii="Pill"
|
||||
onClick={requestClose}
|
||||
before={<Icon size="100" src={Icons.ArrowLeft} />}
|
||||
>
|
||||
<Text size="T300">Permissions</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="Surface">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<Scroll hideTrack visibility="Hover">
|
||||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
<Box direction="Column" gap="100">
|
||||
<Box alignItems="Baseline" gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Power Levels</Text>
|
||||
<BetaNoticeBadge />
|
||||
</Box>
|
||||
<SequenceCard
|
||||
variant="SurfaceVariant"
|
||||
className={SequenceCardStyle}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<SettingTile
|
||||
title="New Power Level"
|
||||
description="Create a new power level."
|
||||
after={
|
||||
!createTag && (
|
||||
<Button
|
||||
onClick={() => setCreateTag(true)}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
outlined
|
||||
disabled={applyingChanges}
|
||||
>
|
||||
<Text size="B300">Create</Text>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{createTag && (
|
||||
<EditPower
|
||||
maxPower={maxPower}
|
||||
onSave={handleSaveTag}
|
||||
onClose={() => setCreateTag(false)}
|
||||
/>
|
||||
)}
|
||||
</SequenceCard>
|
||||
{getPowers(powerTags).map((power) => {
|
||||
const tag = powerTags[power];
|
||||
const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon);
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
key={power}
|
||||
variant={deleted.has(power) ? 'Critical' : 'SurfaceVariant'}
|
||||
className={SequenceCardStyle}
|
||||
direction="Column"
|
||||
gap="400"
|
||||
>
|
||||
<UseStateProvider initial={false}>
|
||||
{(edit, setEdit) =>
|
||||
edit ? (
|
||||
<EditPower
|
||||
maxPower={maxPower}
|
||||
power={power}
|
||||
tag={tag}
|
||||
onSave={handleSaveTag}
|
||||
onClose={() => setEdit(false)}
|
||||
/>
|
||||
) : (
|
||||
<SettingTile
|
||||
before={<PowerColorBadge color={tag.color} />}
|
||||
title={
|
||||
<Box as="span" alignItems="Center" gap="200">
|
||||
<b>{deleted.has(power) ? <s>{tag.name}</s> : tag.name}</b>
|
||||
<Box as="span" shrink="No" alignItems="Inherit" gap="Inherit">
|
||||
{tagIconSrc && <PowerIcon size="50" iconSrc={tagIconSrc} />}
|
||||
<Text as="span" size="T200" priority="300">
|
||||
({power})
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
after={
|
||||
deleted.has(power) ? (
|
||||
<Chip
|
||||
variant="Critical"
|
||||
radii="Pill"
|
||||
disabled={applyingChanges}
|
||||
onClick={() => handleToggleDelete(power)}
|
||||
>
|
||||
<Text size="B300">Undo</Text>
|
||||
</Chip>
|
||||
) : (
|
||||
<Box shrink="No" alignItems="Center" gap="200">
|
||||
<TooltipProvider
|
||||
tooltip={
|
||||
<Tooltip style={{ maxWidth: toRem(200) }}>
|
||||
{usedPowers.has(power) ? (
|
||||
<Box direction="Column">
|
||||
<Text size="L400">Used Power Level</Text>
|
||||
<Text size="T200">
|
||||
You have to remove its use before you can delete it.
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Text>Delete</Text>
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<Chip
|
||||
ref={triggerRef}
|
||||
variant="Secondary"
|
||||
fill="None"
|
||||
radii="Pill"
|
||||
disabled={applyingChanges}
|
||||
aria-disabled={usedPowers.has(power)}
|
||||
onClick={
|
||||
usedPowers.has(power)
|
||||
? undefined
|
||||
: () => handleToggleDelete(power)
|
||||
}
|
||||
>
|
||||
<Icon size="50" src={Icons.Delete} />
|
||||
</Chip>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
<Chip
|
||||
variant="Secondary"
|
||||
radii="Pill"
|
||||
disabled={applyingChanges}
|
||||
onClick={() => setEdit(true)}
|
||||
>
|
||||
<Text size="B300">Edit</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</UseStateProvider>
|
||||
</SequenceCard>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
{hasChanges && (
|
||||
<Menu
|
||||
style={{
|
||||
position: 'sticky',
|
||||
padding: config.space.S200,
|
||||
paddingLeft: config.space.S400,
|
||||
bottom: config.space.S400,
|
||||
left: config.space.S400,
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
variant="Success"
|
||||
>
|
||||
<Box alignItems="Center" gap="400">
|
||||
<Box grow="Yes" direction="Column">
|
||||
{applyState.status === AsyncStatus.Error ? (
|
||||
<Text size="T200">
|
||||
<b>Failed to apply changes! Please try again.</b>
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="T200">
|
||||
<b>Changes saved! Apply when ready.</b>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box shrink="No" gap="200">
|
||||
<Button
|
||||
size="300"
|
||||
variant="Success"
|
||||
fill="None"
|
||||
radii="300"
|
||||
disabled={applyingChanges}
|
||||
onClick={resetChanges}
|
||||
>
|
||||
<Text size="B300">Reset</Text>
|
||||
</Button>
|
||||
<Button
|
||||
size="300"
|
||||
variant="Success"
|
||||
radii="300"
|
||||
disabled={applyingChanges}
|
||||
before={
|
||||
applyingChanges && <Spinner variant="Success" fill="Solid" size="100" />
|
||||
}
|
||||
onClick={handleApplyChanges}
|
||||
>
|
||||
<Text size="B300">Apply Changes</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu>
|
||||
)}
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
1
src/app/features/room-settings/permissions/index.ts
Normal file
1
src/app/features/room-settings/permissions/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Permissions';
|
||||
218
src/app/features/room-settings/permissions/usePermissionItems.ts
Normal file
218
src/app/features/room-settings/permissions/usePermissionItems.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useMemo } from 'react';
|
||||
import { PermissionLocation } from '../../../hooks/usePowerLevels';
|
||||
import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
|
||||
|
||||
export type PermissionItem = {
|
||||
location: PermissionLocation;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type PermissionGroup = {
|
||||
name: string;
|
||||
items: PermissionItem[];
|
||||
};
|
||||
|
||||
export const usePermissionGroups = (): PermissionGroup[] => {
|
||||
const groups: PermissionGroup[] = useMemo(() => {
|
||||
const messagesGroup: PermissionGroup = {
|
||||
name: 'Messages',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
key: MessageEvent.RoomMessage,
|
||||
},
|
||||
name: 'Send Messages',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
key: MessageEvent.Sticker,
|
||||
},
|
||||
name: 'Send Stickers',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
key: MessageEvent.Reaction,
|
||||
},
|
||||
name: 'Send Reactions',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
notification: true,
|
||||
key: 'room',
|
||||
},
|
||||
name: 'Ping @room',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomPinnedEvents,
|
||||
},
|
||||
name: 'Pin Messages',
|
||||
},
|
||||
{
|
||||
location: {},
|
||||
name: 'Other Message Events',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const moderationGroup: PermissionGroup = {
|
||||
name: 'Moderation',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
action: true,
|
||||
key: 'invite',
|
||||
},
|
||||
name: 'Invite',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
action: true,
|
||||
key: 'kick',
|
||||
},
|
||||
name: 'Kick',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
action: true,
|
||||
key: 'ban',
|
||||
},
|
||||
name: 'Ban',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
action: true,
|
||||
key: 'redact',
|
||||
},
|
||||
name: 'Delete Others Messages',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
key: MessageEvent.RoomRedaction,
|
||||
},
|
||||
name: 'Delete Self Messages',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const roomOverviewGroup: PermissionGroup = {
|
||||
name: 'Room Overview',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomAvatar,
|
||||
},
|
||||
name: 'Room Avatar',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomName,
|
||||
},
|
||||
name: 'Room Name',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomTopic,
|
||||
},
|
||||
name: 'Room Topic',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const roomSettingsGroup: PermissionGroup = {
|
||||
name: 'Settings',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomJoinRules,
|
||||
},
|
||||
name: 'Change Room Access',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomCanonicalAlias,
|
||||
},
|
||||
name: 'Publish Address',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomPowerLevels,
|
||||
},
|
||||
name: 'Change All Permission',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.PowerLevelTags,
|
||||
},
|
||||
name: 'Edit Power Levels',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomEncryption,
|
||||
},
|
||||
name: 'Enable Encryption',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomHistoryVisibility,
|
||||
},
|
||||
name: 'History Visibility',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomTombstone,
|
||||
},
|
||||
name: 'Upgrade Room',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
},
|
||||
name: 'Other Settings',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const otherSettingsGroup: PermissionGroup = {
|
||||
name: 'Other',
|
||||
items: [
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: StateEvent.RoomServerAcl,
|
||||
},
|
||||
name: 'Change Server ACLs',
|
||||
},
|
||||
{
|
||||
location: {
|
||||
state: true,
|
||||
key: 'im.vector.modular.widgets',
|
||||
},
|
||||
name: 'Modify Widgets',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return [
|
||||
messagesGroup,
|
||||
moderationGroup,
|
||||
roomOverviewGroup,
|
||||
roomSettingsGroup,
|
||||
otherSettingsGroup,
|
||||
];
|
||||
}, []);
|
||||
|
||||
return groups;
|
||||
};
|
||||
Reference in New Issue
Block a user