Add new space settings (#2293)

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

@ -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,
});