Add new space settings ()

This commit is contained in:
Ajay Bura 2025-03-27 19:54:13 +11:00 committed by GitHub
parent 4aed4d7472
commit 5c39a36c12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 691 additions and 63 deletions

View file

@ -29,6 +29,17 @@ export const useRoomJoinRuleIcon = (): JoinRuleIcons =>
}),
[]
);
export const useSpaceJoinRuleIcon = (): JoinRuleIcons =>
useMemo(
() => ({
[JoinRule.Invite]: Icons.SpaceLock,
[JoinRule.Knock]: Icons.SpaceLock,
[JoinRule.Restricted]: Icons.Space,
[JoinRule.Public]: Icons.SpaceGlobe,
[JoinRule.Private]: Icons.SpaceLock,
}),
[]
);
type JoinRuleLabels = Record<JoinRule, string>;
export const useRoomJoinRuleLabel = (): JoinRuleLabels =>

View file

@ -18,7 +18,7 @@ import { MatrixError } from 'matrix-js-sdk';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { SettingTile } from '../../../components/setting-tile';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRoom } from '../../../hooks/useRoom';
import {
@ -65,7 +65,7 @@ export function RoomPublishedAddresses({ powerLevels }: RoomPublishedAddressesPr
title="Published Addresses"
description={
<span>
If room access is <b>Public</b>, Published addresses will be used to join by anyone.
If access is <b>Public</b>, Published addresses will be used to join by anyone.
</span>
}
/>

View file

@ -19,7 +19,7 @@ import React, { useCallback, useState } from 'react';
import { MatrixError } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { useMatrixClient } from '../../../hooks/useMatrixClient';

View file

@ -16,7 +16,7 @@ import { HistoryVisibility, MatrixError } from 'matrix-js-sdk';
import { RoomHistoryVisibilityEventContent } from 'matrix-js-sdk/lib/types';
import FocusTrap from 'focus-trap-react';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { useMatrixClient } from '../../../hooks/useMatrixClient';

View file

@ -7,9 +7,10 @@ import {
JoinRulesSwitcher,
useRoomJoinRuleIcon,
useRoomJoinRuleLabel,
useSpaceJoinRuleIcon,
} from '../../../components/JoinRulesSwitcher';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRoom } from '../../../hooks/useRoom';
@ -60,6 +61,7 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
}, [allowRestricted, allowKnock, space]);
const icons = useRoomJoinRuleIcon();
const spaceIcons = useSpaceJoinRuleIcon();
const labels = useRoomJoinRuleLabel();
const [submitState, submit] = useAsyncCallback(
@ -99,11 +101,15 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
gap="400"
>
<SettingTile
title="Room Access"
description="Change how people can join the room."
title={room.isSpaceRoom() ? 'Space Access' : 'Room Access'}
description={
room.isSpaceRoom()
? 'Change how people can join the space.'
: 'Change how people can join the room.'
}
after={
<JoinRulesSwitcher
icons={icons}
icons={room.isSpaceRoom() ? spaceIcons : icons}
labels={labels}
rules={joinRules}
value={rule}

View file

@ -17,7 +17,7 @@ import Linkify from 'linkify-react';
import classNames from 'classnames';
import { JoinRule, MatrixError } from 'matrix-js-sdk';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { useRoom } from '../../../hooks/useRoom';
import {
useRoomAvatar,
@ -198,7 +198,12 @@ export function RoomProfileEdit({
src={avatarUrl}
alt={name}
renderFallback={() => (
<RoomIcon size="400" joinRule={joinRule?.join_rule ?? JoinRule.Invite} filled />
<RoomIcon
space={room.isSpaceRoom()}
size="400"
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
filled
/>
)}
/>
</Avatar>
@ -338,7 +343,12 @@ export function RoomProfile({ powerLevels }: RoomProfileProps) {
src={avatarUrl}
alt={name}
renderFallback={() => (
<RoomIcon size="400" joinRule={joinRule?.join_rule ?? JoinRule.Invite} filled />
<RoomIcon
space={room.isSpaceRoom()}
size="400"
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
filled
/>
)}
/>
</Avatar>

View file

@ -2,7 +2,7 @@ import React from 'react';
import { Box, color, Spinner, Switch, Text } from 'folds';
import { MatrixError } from 'matrix-js-sdk';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useRoom } from '../../../hooks/useRoom';
import { useRoomDirectoryVisibility } from '../../../hooks/useRoomDirectoryVisibility';

View file

@ -20,7 +20,7 @@ import FocusTrap from 'focus-trap-react';
import { MatrixError } from 'matrix-js-sdk';
import { RoomCreateEventContent, RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useRoom } from '../../../hooks/useRoom';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
@ -39,7 +39,7 @@ type RoomUpgradeProps = {
export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
const mx = useMatrixClient();
const room = useRoom();
const { navigateRoom } = useRoomNavigate();
const { navigateRoom, navigateSpace } = useRoomNavigate();
const createContent = useStateEvent(
room,
StateEvent.RoomCreate
@ -66,14 +66,22 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
const handleOpenRoom = () => {
if (replacementRoom) {
requestClose();
navigateRoom(replacementRoom);
if (room.isSpaceRoom()) {
navigateSpace(replacementRoom);
} else {
navigateRoom(replacementRoom);
}
}
};
const handleOpenOldRoom = () => {
if (predecessorRoomId) {
requestClose();
navigateRoom(predecessorRoomId, createContent.predecessor?.event_id);
if (room.isSpaceRoom()) {
navigateSpace(predecessorRoomId);
} else {
navigateRoom(predecessorRoomId, createContent.predecessor?.event_id);
}
}
};
@ -110,10 +118,11 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
gap="400"
>
<SettingTile
title="Upgrade Room"
title={room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}
description={
replacementRoom
? tombstoneContent.body || 'This room has been replaced!'
? tombstoneContent.body ||
`This ${room.isSpaceRoom() ? 'space' : 'room'} has been replaced!`
: `Current room version: ${roomVersion}.`
}
after={
@ -127,7 +136,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
radii="300"
onClick={handleOpenOldRoom}
>
<Text size="B300">Old Room</Text>
<Text size="B300">{room.isSpaceRoom() ? 'Old Space' : 'Old Room'}</Text>
</Button>
)}
{replacementRoom ? (
@ -138,7 +147,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
radii="300"
onClick={handleOpenRoom}
>
<Text size="B300">Open New Room</Text>
<Text size="B300">{room.isSpaceRoom() ? 'Open New Space' : 'Open New Room'}</Text>
</Button>
) : (
<Button
@ -183,7 +192,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
size="500"
>
<Box grow="Yes">
<Text size="H4">Room Upgrade</Text>
<Text size="H4">{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}</Text>
</Box>
<IconButton size="300" onClick={() => setPrompt(false)} radii="300">
<Icon src={Icons.Cross} />
@ -203,7 +212,9 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
/>
</Box>
<Button type="submit" variant="Secondary">
<Text size="B400">Upgrade Room</Text>
<Text size="B400">
{room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}
</Text>
</Button>
</Box>
</Dialog>

View file

@ -0,0 +1,7 @@
export * from './RoomAddress';
export * from './RoomEncryption';
export * from './RoomHistoryVisibility';
export * from './RoomJoinRules';
export * from './RoomProfile';
export * from './RoomPublish';
export * from './RoomUpgrade';

View file

@ -12,7 +12,7 @@ import {
PermissionLocation,
usePowerLevelsAPI,
} from '../../../hooks/usePowerLevels';
import { usePermissionGroups } from './usePermissionItems';
import { PermissionGroup } from './types';
import { getPowers, usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { useRoom } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
@ -27,8 +27,9 @@ const USER_DEFAULT_LOCATION: PermissionLocation = {
type PermissionGroupsProps = {
powerLevels: IPowerLevels;
permissionGroups: PermissionGroup[];
};
export function PermissionGroups({ powerLevels }: PermissionGroupsProps) {
export function PermissionGroups({ powerLevels, permissionGroups }: PermissionGroupsProps) {
const mx = useMatrixClient();
const room = useRoom();
const alive = useAlive();
@ -40,8 +41,6 @@ export function PermissionGroups({ powerLevels }: PermissionGroupsProps) {
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()
);

View file

@ -24,16 +24,16 @@ 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';
import { PermissionGroup } from './types';
type PeekPermissionsProps = {
powerLevels: IPowerLevels;
power: number;
permissionGroups: PermissionGroup[];
children: (handleOpen: MouseEventHandler<HTMLButtonElement>, opened: boolean) => ReactNode;
};
function PeekPermissions({ powerLevels, power, children }: PeekPermissionsProps) {
function PeekPermissions({ powerLevels, power, permissionGroups, children }: PeekPermissionsProps) {
const [menuCords, setMenuCords] = useState<RectCords>();
const permissionGroups = usePermissionGroups();
const handleOpen: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuCords(evt.currentTarget.getBoundingClientRect());
@ -101,9 +101,10 @@ function PeekPermissions({ powerLevels, power, children }: PeekPermissionsProps)
type PowersProps = {
powerLevels: IPowerLevels;
permissionGroups: PermissionGroup[];
onEdit?: () => void;
};
export function Powers({ powerLevels, onEdit }: PowersProps) {
export function Powers({ powerLevels, permissionGroups, onEdit }: PowersProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const room = useRoom();
@ -144,7 +145,12 @@ export function Powers({ powerLevels, onEdit }: PowersProps) {
const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon);
return (
<PeekPermissions key={power} powerLevels={powerLevels} power={power}>
<PeekPermissions
key={power}
powerLevels={powerLevels}
power={power}
permissionGroups={permissionGroups}
>
{(openMenu, opened) => (
<Chip
onClick={openMenu}

View file

@ -0,0 +1,4 @@
export * from './PermissionGroups';
export * from './Powers';
export * from './PowersEditor';
export * from './types';

View file

@ -0,0 +1,12 @@
import { PermissionLocation } from '../../../hooks/usePowerLevels';
export type PermissionItem = {
location: PermissionLocation;
name: string;
description?: string;
};
export type PermissionGroup = {
name: string;
items: PermissionItem[];
};

View file

@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
import { config } from 'folds';
export const SequenceCardStyle = style({
padding: config.space.S300,
});

View file

@ -18,7 +18,7 @@ import {
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
import { openInviteUser, openSpaceSettings } from '../../../client/action/navigation';
import { openInviteUser } from '../../../client/action/navigation';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
@ -26,6 +26,7 @@ import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { stopPropagation } from '../../utils/keyboard';
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
type HierarchyItemWithParent = HierarchyItem & {
parentId: string;
@ -153,11 +154,12 @@ function SettingsMenuItem({
disabled?: boolean;
}) {
const openRoomSettings = useOpenRoomSettings();
const openSpaceSettings = useOpenSpaceSettings();
const space = useSpaceOptionally();
const handleSettings = () => {
if ('space' in item) {
openSpaceSettings(item.roomId);
openSpaceSettings(item.roomId, item.parentId);
} else {
openRoomSettings(item.roomId, space?.roomId);
}

View file

@ -26,7 +26,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
import { RoomAvatar } from '../../components/room-avatar';
import { nameInitials } from '../../utils/common';
import * as css from './LobbyHeader.css';
import { openInviteUser, openSpaceSettings } from '../../../client/action/navigation';
import { openInviteUser } from '../../../client/action/navigation';
import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
@ -35,6 +35,7 @@ import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { BackRouteHandler } from '../../components/BackRouteHandler';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
type LobbyMenuProps = {
roomId: string;
@ -46,6 +47,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
const mx = useMatrixClient();
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const openSpaceSettings = useOpenSpaceSettings();
const handleInvite = () => {
openInviteUser(roomId);
@ -132,7 +134,9 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
const name = useRoomName(space);
const avatarMxc = useRoomAvatar(space);
const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());

View file

@ -11,12 +11,12 @@ import { useRoomAvatar, useRoomJoinRule, useRoomName } from '../../hooks/useRoom
import { mDirectAtom } from '../../state/mDirectList';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { General } from './general';
import { Members } from './members';
import { EmojisStickers } from './emojis-stickers';
import { Members } from '../common-settings/members';
import { EmojisStickers } from '../common-settings/emojis-stickers';
import { Permissions } from './permissions';
import { RoomSettingsPage } from '../../state/roomSettings';
import { useRoom } from '../../hooks/useRoom';
import { DeveloperTools } from './developer-tools';
import { DeveloperTools } from '../common-settings/developer-tools';
type RoomSettingsMenuItem = {
page: RoomSettingsPage;

View file

@ -1,15 +1,18 @@
import React from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { RoomProfile } from './RoomProfile';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useRoom } from '../../../hooks/useRoom';
import { RoomEncryption } from './RoomEncryption';
import { RoomHistoryVisibility } from './RoomHistoryVisibility';
import { RoomJoinRules } from './RoomJoinRules';
import { RoomLocalAddresses, RoomPublishedAddresses } from './RoomAddress';
import { RoomPublish } from './RoomPublish';
import { RoomUpgrade } from './RoomUpgrade';
import {
RoomProfile,
RoomEncryption,
RoomHistoryVisibility,
RoomJoinRules,
RoomLocalAddresses,
RoomPublishedAddresses,
RoomPublish,
RoomUpgrade,
} from '../../common-settings/general';
type GeneralProps = {
requestClose: () => void;

View file

@ -1,13 +1,12 @@
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';
import { usePermissionGroups } from './usePermissionItems';
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
type PermissionsProps = {
requestClose: () => void;
@ -21,6 +20,7 @@ export function Permissions({ requestClose }: PermissionsProps) {
StateEvent.PowerLevelTags,
getPowerLevel(mx.getSafeUserId())
);
const permissionGroups = usePermissionGroups();
const [powerEditor, setPowerEditor] = useState(false);
@ -55,8 +55,9 @@ export function Permissions({ requestClose }: PermissionsProps) {
<Powers
powerLevels={powerLevels}
onEdit={canEditPowers ? handleEditPowers : undefined}
permissionGroups={permissionGroups}
/>
<PermissionGroups powerLevels={powerLevels} />
<PermissionGroups powerLevels={powerLevels} permissionGroups={permissionGroups} />
</Box>
</PageContent>
</Scroll>

View file

@ -1,17 +1,6 @@
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[];
};
import { PermissionGroup } from '../../common-settings/permissions';
export const usePermissionGroups = (): PermissionGroup[] => {
const groups: PermissionGroup[] = useMemo(() => {

View file

@ -0,0 +1,173 @@
import React, { useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import { Avatar, Box, config, Icon, IconButton, Icons, IconSrc, MenuItem, Text } from 'folds';
import { JoinRule } from 'matrix-js-sdk';
import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../components/page';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useRoomAvatar, useRoomJoinRule, useRoomName } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { SpaceSettingsPage } from '../../state/spaceSettings';
import { useRoom } from '../../hooks/useRoom';
import { EmojisStickers } from '../common-settings/emojis-stickers';
import { Members } from '../common-settings/members';
import { DeveloperTools } from '../common-settings/developer-tools';
import { General } from './general';
import { Permissions } from './permissions';
type SpaceSettingsMenuItem = {
page: SpaceSettingsPage;
name: string;
icon: IconSrc;
};
const useSpaceSettingsMenuItems = (): SpaceSettingsMenuItem[] =>
useMemo(
() => [
{
page: SpaceSettingsPage.GeneralPage,
name: 'General',
icon: Icons.Setting,
},
{
page: SpaceSettingsPage.MembersPage,
name: 'Members',
icon: Icons.User,
},
{
page: SpaceSettingsPage.PermissionsPage,
name: 'Permissions',
icon: Icons.Lock,
},
{
page: SpaceSettingsPage.EmojisStickersPage,
name: 'Emojis & Stickers',
icon: Icons.Smile,
},
{
page: SpaceSettingsPage.DeveloperToolsPage,
name: 'Developer Tools',
icon: Icons.Terminal,
},
],
[]
);
type SpaceSettingsProps = {
initialPage?: SpaceSettingsPage;
requestClose: () => void;
};
export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps) {
const room = useRoom();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const mDirects = useAtomValue(mDirectAtom);
const roomAvatar = useRoomAvatar(room, mDirects.has(room.roomId));
const roomName = useRoomName(room);
const joinRuleContent = useRoomJoinRule(room);
const avatarUrl = roomAvatar
? mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined
: undefined;
const screenSize = useScreenSizeContext();
const [activePage, setActivePage] = useState<SpaceSettingsPage | undefined>(() => {
if (initialPage) return initialPage;
return screenSize === ScreenSize.Mobile ? undefined : SpaceSettingsPage.GeneralPage;
});
const menuItems = useSpaceSettingsMenuItems();
const handlePageRequestClose = () => {
if (screenSize === ScreenSize.Mobile) {
setActivePage(undefined);
return;
}
requestClose();
};
return (
<PageRoot
nav={
screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
<PageNav size="300">
<PageNavHeader outlined={false}>
<Box grow="Yes" gap="200">
<Avatar size="200" radii="300">
<RoomAvatar
roomId={room.roomId}
src={avatarUrl}
alt={roomName}
renderFallback={() => (
<RoomIcon
space
size="50"
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
filled
/>
)}
/>
</Avatar>
<Text size="H4" truncate>
{roomName}
</Text>
</Box>
<Box shrink="No">
{screenSize === ScreenSize.Mobile && (
<IconButton onClick={requestClose} variant="Background">
<Icon src={Icons.Cross} />
</IconButton>
)}
</Box>
</PageNavHeader>
<Box grow="Yes" direction="Column">
<PageNavContent>
<div style={{ flexGrow: 1 }}>
{menuItems.map((item) => (
<MenuItem
key={item.name}
variant="Background"
radii="400"
aria-pressed={activePage === item.page}
before={<Icon src={item.icon} size="100" filled={activePage === item.page} />}
onClick={() => setActivePage(item.page)}
>
<Text
style={{
fontWeight: activePage === item.page ? config.fontWeight.W600 : undefined,
}}
size="T300"
truncate
>
{item.name}
</Text>
</MenuItem>
))}
</div>
</PageNavContent>
</Box>
</PageNav>
)
}
>
{activePage === SpaceSettingsPage.GeneralPage && (
<General requestClose={handlePageRequestClose} />
)}
{activePage === SpaceSettingsPage.MembersPage && (
<Members requestClose={handlePageRequestClose} />
)}
{activePage === SpaceSettingsPage.PermissionsPage && (
<Permissions requestClose={handlePageRequestClose} />
)}
{activePage === SpaceSettingsPage.EmojisStickersPage && (
<EmojisStickers requestClose={handlePageRequestClose} />
)}
{activePage === SpaceSettingsPage.DeveloperToolsPage && (
<DeveloperTools requestClose={handlePageRequestClose} />
)}
</PageRoot>
);
}

View file

@ -0,0 +1,39 @@
import React from 'react';
import { SpaceSettings } from './SpaceSettings';
import { Modal500 } from '../../components/Modal500';
import { useCloseSpaceSettings, useSpaceSettingsState } from '../../state/hooks/spaceSettings';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { SpaceSettingsState } from '../../state/spaceSettings';
import { RoomProvider } from '../../hooks/useRoom';
import { SpaceProvider } from '../../hooks/useSpace';
type RenderSettingsProps = {
state: SpaceSettingsState;
};
function RenderSettings({ state }: RenderSettingsProps) {
const { roomId, spaceId, page } = state;
const closeSettings = useCloseSpaceSettings();
const allJoinedRooms = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allJoinedRooms);
const room = getRoom(roomId);
const space = spaceId ? getRoom(spaceId) : undefined;
if (!room) return null;
return (
<Modal500 requestClose={closeSettings}>
<SpaceProvider value={space ?? null}>
<RoomProvider value={room}>
<SpaceSettings initialPage={page} requestClose={closeSettings} />
</RoomProvider>
</SpaceProvider>
</Modal500>
);
}
export function SpaceSettingsRenderer() {
const state = useSpaceSettingsState();
if (!state) return null;
return <RenderSettings state={state} />;
}

View file

@ -0,0 +1,63 @@
import React from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useRoom } from '../../../hooks/useRoom';
import {
RoomProfile,
RoomJoinRules,
RoomLocalAddresses,
RoomPublishedAddresses,
RoomPublish,
RoomUpgrade,
} from '../../common-settings/general';
type GeneralProps = {
requestClose: () => void;
};
export function General({ requestClose }: GeneralProps) {
const room = useRoom();
const powerLevels = usePowerLevels(room);
return (
<Page>
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
General
</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">
<RoomProfile powerLevels={powerLevels} />
<Box direction="Column" gap="100">
<Text size="L400">Options</Text>
<RoomJoinRules powerLevels={powerLevels} />
<RoomPublish powerLevels={powerLevels} />
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Addresses</Text>
<RoomPublishedAddresses powerLevels={powerLevels} />
<RoomLocalAddresses powerLevels={powerLevels} />
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Advance Options</Text>
<RoomUpgrade powerLevels={powerLevels} requestClose={requestClose} />
</Box>
</Box>
</PageContent>
</Scroll>
</Box>
</Page>
);
}

View file

@ -0,0 +1 @@
export * from './General';

View file

@ -0,0 +1,2 @@
export * from './SpaceSettings';
export * from './SpaceSettingsRenderer';

View file

@ -0,0 +1,67 @@
import React, { useState } from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { StateEvent } from '../../../../types/matrix/room';
import { usePermissionGroups } from './usePermissionItems';
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
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 permissionGroups = usePermissionGroups();
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={permissionGroups}
/>
<PermissionGroups powerLevels={powerLevels} permissionGroups={permissionGroups} />
</Box>
</PageContent>
</Scroll>
</Box>
</Page>
);
}

View file

@ -0,0 +1 @@
export * from './Permissions';

View file

@ -0,0 +1,148 @@
import { useMemo } from 'react';
import { StateEvent } from '../../../../types/matrix/room';
import { PermissionGroup } from '../../common-settings/permissions';
export const usePermissionGroups = (): PermissionGroup[] => {
const groups: PermissionGroup[] = useMemo(() => {
const messagesGroup: PermissionGroup = {
name: 'Manage',
items: [
{
location: {
state: true,
key: StateEvent.SpaceChild,
},
name: 'Manage space rooms',
},
{
location: {},
name: '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',
},
],
};
const roomOverviewGroup: PermissionGroup = {
name: 'Space Overview',
items: [
{
location: {
state: true,
key: StateEvent.RoomAvatar,
},
name: 'Space Avatar',
},
{
location: {
state: true,
key: StateEvent.RoomName,
},
name: 'Space Name',
},
{
location: {
state: true,
key: StateEvent.RoomTopic,
},
name: 'Space Topic',
},
],
};
const roomSettingsGroup: PermissionGroup = {
name: 'Settings',
items: [
{
location: {
state: true,
key: StateEvent.RoomJoinRules,
},
name: 'Change Space 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.RoomTombstone,
},
name: 'Upgrade Space',
},
{
location: {
state: true,
},
name: 'Other Settings',
},
],
};
const otherSettingsGroup: PermissionGroup = {
name: 'Other',
items: [
{
location: {
state: true,
key: StateEvent.RoomServerAcl,
},
name: 'Change Server ACLs',
},
],
};
return [
messagesGroup,
moderationGroup,
roomOverviewGroup,
roomSettingsGroup,
otherSettingsGroup,
];
}, []);
return groups;
};

View file

@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
import { config } from 'folds';
export const SequenceCardStyle = style({
padding: config.space.S300,
});

View file

@ -60,6 +60,7 @@ import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification'
import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
import { RoomSettingsRenderer } from '../features/room-settings';
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
import { SpaceSettingsRenderer } from '../features/space-settings';
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
const { hashRouter } = clientConfig;
@ -125,6 +126,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
<Outlet />
</ClientLayout>
<RoomSettingsRenderer />
<SpaceSettingsRenderer />
<ReceiveSelfDeviceVerification />
<AutoRestoreBackupOnVerification />
</ClientNonUIFeatures>

View file

@ -82,7 +82,7 @@ import { useRoomsUnread } from '../../../state/hooks/unread';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { markAsRead } from '../../../../client/action/notifications';
import { copyToClipboard } from '../../../utils/dom';
import { openInviteUser, openSpaceSettings } from '../../../../client/action/navigation';
import { openInviteUser } from '../../../../client/action/navigation';
import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoom } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers';
@ -90,6 +90,7 @@ import { getRoomAvatarUrl } from '../../../utils/room';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
type SpaceMenuProps = {
room: Room;
@ -104,6 +105,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const openSpaceSettings = useOpenSpaceSettings();
const allChild = useSpaceChildren(
allRoomsAtom,

View file

@ -54,7 +54,7 @@ import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import { openInviteUser, openSpaceSettings } from '../../../../client/action/navigation';
import { openInviteUser } from '../../../../client/action/navigation';
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { markAsRead } from '../../../../client/action/notifications';
@ -74,6 +74,7 @@ import {
getRoomNotificationMode,
useRoomsNotificationPreferencesContext,
} from '../../../hooks/useRoomsNotificationPreferences';
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
type SpaceMenuProps = {
room: Room;
@ -86,6 +87,7 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const openSpaceSettings = useOpenSpaceSettings();
const allChild = useSpaceChildren(
allRoomsAtom,

View file

@ -0,0 +1,34 @@
import { useCallback } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { spaceSettingsAtom, SpaceSettingsPage, SpaceSettingsState } from '../spaceSettings';
export const useSpaceSettingsState = (): SpaceSettingsState | undefined => {
const data = useAtomValue(spaceSettingsAtom);
return data;
};
type CloseCallback = () => void;
export const useCloseSpaceSettings = (): CloseCallback => {
const setSettings = useSetAtom(spaceSettingsAtom);
const close: CloseCallback = useCallback(() => {
setSettings(undefined);
}, [setSettings]);
return close;
};
type OpenCallback = (roomId: string, space?: string, page?: SpaceSettingsPage) => void;
export const useOpenSpaceSettings = (): OpenCallback => {
const setSettings = useSetAtom(spaceSettingsAtom);
const open: OpenCallback = useCallback(
(roomId, spaceId, page) => {
setSettings({ roomId, spaceId, page });
},
[setSettings]
);
return open;
};

View file

@ -0,0 +1,17 @@
import { atom } from 'jotai';
export enum SpaceSettingsPage {
GeneralPage,
MembersPage,
PermissionsPage,
EmojisStickersPage,
DeveloperToolsPage,
}
export type SpaceSettingsState = {
page?: SpaceSettingsPage;
roomId: string;
spaceId?: string;
};
export const spaceSettingsAtom = atom<SpaceSettingsState | undefined>(undefined);