refactor(room-settings): collapse room settings into a single Modal500 page, dropping the per-room permissions and developer surfaces

This commit is contained in:
heaven 2026-06-15 02:57:32 +03:00
parent abae93170a
commit caff65d9ee
15 changed files with 208 additions and 679 deletions

View file

@ -0,0 +1,54 @@
import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
// Settings-window shell (room / space settings) — the self-styled Vojo card
// vocabulary (AiChatPrivacy / LogoutDialog): own surface, 16px radius,
// hairline border, deep shadow. Replaces the stock folds `Modal` whose flat
// square-ish chrome read as the old cinny dialog. The card only provides
// the shell; the content inside (PageRoot nav + page) paints its own panel
// tones.
export const Card = style({
width: `calc(100vw - 2 * ${config.space.S400})`,
maxWidth: toRem(860),
height: '85vh',
maxHeight: toRem(700),
display: 'flex',
flexDirection: 'column',
minHeight: 0,
overflow: 'hidden',
backgroundColor: color.Background.Container,
borderRadius: toRem(16),
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.5)',
'@media': {
// Narrow viewports: the window goes (near) full-screen — the rounded
// floating card needs breathing room it doesn't have on a phone.
// `--vojo-safe-top` is reset to 0 INSIDE the card (the TSX), so the
// status-bar clearance comes straight from env() here — the card is
// genuinely top-of-screen in this branch.
'screen and (max-width: 600px)': {
width: '100vw',
maxWidth: '100vw',
height: '100dvh',
maxHeight: '100dvh',
borderRadius: 0,
border: 'none',
paddingTop: 'env(safe-area-inset-top, 0px)',
},
},
});
// Single-pane variant (room settings): no nav column, so the two-pane
// width reads as a stretched empty form. 560px ≈ the page pane's share of
// the wide layout (860 minus the ~300px nav). The media override keeps the
// phone branch full-screen — this class is emitted after `Card`, so its
// base maxWidth would otherwise beat Card's `100vw` media rule in the
// cascade.
export const CardNarrow = style({
maxWidth: toRem(560),
'@media': {
'screen and (max-width: 600px)': {
maxWidth: '100vw',
},
},
});

View file

@ -1,14 +1,23 @@
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import { Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
import { stopPropagation } from '../utils/keyboard';
import { HorseshoeEnabledContext } from './page';
import * as css from './Modal500.css';
type Modal500Props = {
requestClose: () => void;
// Single-pane variant (room settings) — the wide default fits the
// two-pane nav+page shell (space settings).
narrow?: boolean;
children: ReactNode;
};
export function Modal500({ requestClose, children }: Modal500Props) {
// Settings-window shell (room / space settings). A self-styled Vojo card
// (see Modal500.css) instead of the stock folds Modal — near-fullscreen on
// phones, a floating rounded window on desktop.
export function Modal500({ requestClose, narrow, children }: Modal500Props) {
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
@ -20,26 +29,27 @@ export function Modal500({ requestClose, children }: Modal500Props) {
escapeDeactivates: stopPropagation,
}}
>
<Modal
size="500"
variant="Background"
<div
className={classNames(css.Card, narrow && css.CardNarrow)}
role="dialog"
aria-modal="true"
// Reset `--vojo-safe-top` for everything mounted inside the
// dialog. The Android status-bar inset is reserved by each
// page header's `padding-top: var(--vojo-safe-top)` for
// top-of-screen surfaces — but a centred 500px modal sits
// away from the screen edge, and the same padding inside it
// just adds dead space above its header.
// top-of-screen surfaces — on phones this window IS
// top-of-screen, but its own header chrome supplies the
// spacing; the inherited padding would double it.
style={{ ['--vojo-safe-top' as string]: '0px' }}
>
{/* PageRoot rendered inside the dialog (Settings,
SpaceSettings, RoomSettings) would otherwise pick up
the web horseshoe layout void column + rounded
corners inside a fixed 500px shell. Disable horseshoe
corners inside a fixed shell. Disable horseshoe
for everything inside the modal. */}
<HorseshoeEnabledContext.Provider value={false}>
{children}
</HorseshoeEnabledContext.Provider>
</Modal>
</div>
</FocusTrap>
</OverlayCenter>
</Overlay>

View file

@ -2,6 +2,7 @@ import { Chip, config, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text } fr
import React, { MouseEventHandler, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import { isKeyHotkey } from 'is-hotkey';
import { useTranslation } from 'react-i18next';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
import { PowerColorBadge, PowerIcon } from '../power';
import { getPowerTagIconSrc } from '../../hooks/useMemberPowerTag';
@ -10,17 +11,19 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { stopPropagation } from '../../utils/keyboard';
import { useRoom } from '../../hooks/useRoom';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
import { SpaceSettingsPage } from '../../state/spaceSettings';
import { RoomSettingsPage } from '../../state/roomSettings';
// «Manage Powers» only exists for SPACES — the room-settings Permissions
// page was removed with the room-settings redesign (power-level editing is
// Matrix-protocol surface end users never needed in a chat). For ordinary
// rooms the creator chip is a plain, non-interactive label.
export function CreatorChip() {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const room = useRoom();
const space = useSpaceOptionally();
const openRoomSettings = useOpenRoomSettings();
const openSpaceSettings = useOpenSpaceSettings();
const [cords, setCords] = useState<RectCords>();
@ -33,6 +36,31 @@ export function CreatorChip() {
const close = () => setCords(undefined);
const isSpace = room.isSpaceRoom();
const chip = (
<Chip
// Outside spaces the chip is a plain badge — render it as a <span>
// so keyboard users don't tab onto a button that does nothing.
as={isSpace ? 'button' : 'span'}
variant="Success"
outlined
radii="Pill"
before={
cords ? <Icon size="50" src={Icons.ChevronBottom} /> : <PowerColorBadge color={tag.color} />
}
after={tagIconSrc ? <PowerIcon size="50" iconSrc={tagIconSrc} /> : undefined}
onClick={isSpace ? open : undefined}
aria-pressed={isSpace ? !!cords : undefined}
>
<Text size="B300" truncate>
{tag.name}
</Text>
</Chip>
);
if (!isSpace) return chip;
return (
<PopOut
anchor={cords}
@ -58,44 +86,18 @@ export function CreatorChip() {
size="300"
radii="300"
onClick={() => {
if (room.isSpaceRoom()) {
openSpaceSettings(
room.roomId,
space?.roomId,
SpaceSettingsPage.PermissionsPage
);
} else {
openRoomSettings(room.roomId, space?.roomId, RoomSettingsPage.PermissionsPage);
}
openSpaceSettings(room.roomId, space?.roomId, SpaceSettingsPage.PermissionsPage);
close();
}}
>
<Text size="B300">Manage Powers</Text>
<Text size="B300">{t('User.manage_powers')}</Text>
</MenuItem>
</div>
</Menu>
</FocusTrap>
}
>
<Chip
variant="Success"
outlined
radii="Pill"
before={
cords ? (
<Icon size="50" src={Icons.ChevronBottom} />
) : (
<PowerColorBadge color={tag.color} />
)
}
after={tagIconSrc ? <PowerIcon size="50" iconSrc={tagIconSrc} /> : undefined}
onClick={open}
aria-pressed={!!cords}
>
<Text size="B300" truncate>
{tag.name}
</Text>
</Chip>
{chip}
</PopOut>
);
}

View file

@ -23,6 +23,7 @@ import {
import React, { MouseEventHandler, useCallback, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import { isKeyHotkey } from 'is-hotkey';
import { useTranslation } from 'react-i18next';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { PowerColorBadge, PowerIcon } from '../power';
@ -30,8 +31,6 @@ import { useGetMemberPowerLevel, usePowerLevels } from '../../hooks/usePowerLeve
import { getPowers, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { stopPropagation } from '../../utils/keyboard';
import { StateEvent } from '../../../types/matrix/room';
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { RoomSettingsPage } from '../../state/roomSettings';
import { useRoom } from '../../hooks/useRoom';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { CutoutCard } from '../cutout-card';
@ -145,11 +144,11 @@ function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps)
}
export function PowerChip({ userId }: { userId: string }) {
const { t } = useTranslation();
const mx = useMatrixClient();
const room = useRoom();
const space = useSpaceOptionally();
const useAuthentication = useMediaAuthentication();
const openRoomSettings = useOpenRoomSettings();
const openSpaceSettings = useOpenSpaceSettings();
const powerLevels = usePowerLevels(room);
@ -285,33 +284,33 @@ export function PowerChip({ userId }: { userId: string }) {
);
})}
</Box>
<Line size="300" />
<div style={{ padding: config.space.S100 }}>
<MenuItem
variant="Surface"
fill="None"
size="300"
radii="300"
onClick={() => {
if (room.isSpaceRoom()) {
openSpaceSettings(
room.roomId,
space?.roomId,
SpaceSettingsPage.PermissionsPage
);
} else {
openRoomSettings(
room.roomId,
space?.roomId,
RoomSettingsPage.PermissionsPage
);
}
close();
}}
>
<Text size="B300">Manage Powers</Text>
</MenuItem>
</div>
{/* «Manage Powers» only for spaces the room-settings
Permissions page was removed with the room-settings
redesign; role assignment for rooms happens right here
via the tag list above. */}
{room.isSpaceRoom() && (
<>
<Line size="300" />
<div style={{ padding: config.space.S100 }}>
<MenuItem
variant="Surface"
fill="None"
size="300"
radii="300"
onClick={() => {
openSpaceSettings(
room.roomId,
space?.roomId,
SpaceSettingsPage.PermissionsPage
);
close();
}}
>
<Text size="B300">{t('User.manage_powers')}</Text>
</MenuItem>
</div>
</>
)}
</Menu>
</FocusTrap>
}

View file

@ -1,176 +1,68 @@
import React, { useMemo, useState } from 'react';
import { Avatar, Box, Icon, IconButton, Icons, IconSrc, Text } from 'folds';
import React from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { JoinRule } from 'matrix-js-sdk';
import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../components/page';
import {
SettingsNavEyebrow,
SettingsNavItem,
SettingsNavSection,
} from '../common-settings/SettingsNav';
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 { isOneOnOneRoom } from '../../utils/room';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { General } from './general';
import { Members } from '../common-settings/members';
import { EmojisStickers } from '../common-settings/emojis-stickers';
import { Permissions } from './permissions';
import { RoomSettingsPage } from '../../state/roomSettings';
import { Page, PageContent, PageHeader } from '../../components/page';
import { SettingsSection } from '../settings/SettingsSection';
import { usePowerLevels } from '../../hooks/usePowerLevels';
import { useRoom } from '../../hooks/useRoom';
import { DeveloperTools } from '../common-settings/developer-tools';
type RoomSettingsMenuItem = {
page: RoomSettingsPage;
name: string;
icon: IconSrc;
};
const useRoomSettingsMenuItems = (): RoomSettingsMenuItem[] => {
const { t } = useTranslation();
return useMemo(
() => [
{
page: RoomSettingsPage.GeneralPage,
name: t('RoomSettings.general'),
icon: Icons.Setting,
},
{
page: RoomSettingsPage.MembersPage,
name: t('RoomSettings.members'),
icon: Icons.User,
},
{
page: RoomSettingsPage.PermissionsPage,
name: t('RoomSettings.permissions'),
icon: Icons.Lock,
},
{
page: RoomSettingsPage.EmojisStickersPage,
name: t('RoomSettings.emojis_stickers'),
icon: Icons.Smile,
},
{
page: RoomSettingsPage.DeveloperToolsPage,
name: t('RoomSettings.developer_tools'),
icon: Icons.Terminal,
},
],
[t]
);
};
import {
RoomProfile,
RoomEncryption,
RoomHistoryVisibility,
RoomJoinRules,
RoomVoiceMessages,
} from '../common-settings/general';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
type RoomSettingsProps = {
initialPage?: RoomSettingsPage;
requestClose: () => void;
};
export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
// Single-page room settings, opened from the room's «⋯» menu. No nav
// column, no sub-pages: rooms keep only the profile + the four
// user-meaningful toggles. Addresses / directory publish / room upgrade /
// power levels / emoji packs / developer tools are Matrix-protocol
// surfaces end users never needed in a chat — dropped from rooms (spaces
// keep the full set in SpaceSettings).
export function RoomSettings({ requestClose }: RoomSettingsProps) {
const { t } = useTranslation();
const room = useRoom();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
// Peer-avatar fallback follows the member-count gate, mirroring the room
// header (post-P3c). Settings is a globally-mounted modal, so the
// IsOneOnOneProvider context isn't in scope here — call the helper directly.
const roomAvatar = useRoomAvatar(room, isOneOnOneRoom(room));
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<RoomSettingsPage | undefined>(() => {
if (initialPage) return initialPage;
return screenSize === ScreenSize.Mobile ? undefined : RoomSettingsPage.GeneralPage;
});
const menuItems = useRoomSettingsMenuItems();
const handlePageRequestClose = () => {
if (screenSize === ScreenSize.Mobile) {
setActivePage(undefined);
return;
}
requestClose();
};
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
return (
<PageRoot
nav={
screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
<PageNav size="300">
<PageNavHeader outlined={false}>
<Box grow="Yes" gap="300" alignItems="Center">
<Avatar size="300" radii="300">
<RoomAvatar
roomId={room.roomId}
src={avatarUrl}
alt={roomName}
renderFallback={() => (
<RoomIcon
size="50"
roomType={room.getType()}
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
filled
/>
)}
/>
</Avatar>
<Box direction="Column" grow="Yes">
<SettingsNavEyebrow>{t('RoomSettings.settings')}</SettingsNavEyebrow>
<Text size="H4" truncate>
{roomName}
</Text>
</Box>
</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 }}>
<SettingsNavSection>{t('RoomSettings.sections')}</SettingsNavSection>
{menuItems.map((item) => (
<SettingsNavItem
key={item.name}
icon={item.icon}
label={item.name}
active={activePage === item.page}
onClick={() => setActivePage(item.page)}
/>
))}
</div>
</PageNavContent>
<Page>
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
{t('Room.room_settings')}
</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 permissions={permissions} />
<SettingsSection label={t('RoomSettings.options')}>
<RoomJoinRules permissions={permissions} />
<RoomHistoryVisibility permissions={permissions} />
<RoomEncryption permissions={permissions} />
<RoomVoiceMessages permissions={permissions} />
</SettingsSection>
</Box>
</PageNav>
)
}
>
{activePage === RoomSettingsPage.GeneralPage && (
<General requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.MembersPage && (
<Members requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.PermissionsPage && (
<Permissions requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.EmojisStickersPage && (
<EmojisStickers requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.DeveloperToolsPage && (
<DeveloperTools requestClose={handlePageRequestClose} />
)}
</PageRoot>
</PageContent>
</Scroll>
</Box>
</Page>
);
}

View file

@ -11,7 +11,7 @@ type RenderSettingsProps = {
state: RoomSettingsState;
};
function RenderSettings({ state }: RenderSettingsProps) {
const { roomId, spaceId, page } = state;
const { roomId, spaceId } = state;
const closeSettings = useCloseRoomSettings();
const allJoinedRooms = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allJoinedRooms);
@ -21,10 +21,10 @@ function RenderSettings({ state }: RenderSettingsProps) {
if (!room) return null;
return (
<Modal500 requestClose={closeSettings}>
<Modal500 narrow requestClose={closeSettings}>
<SpaceProvider value={space ?? null}>
<RoomProvider value={room}>
<RoomSettings initialPage={page} requestClose={closeSettings} />
<RoomSettings requestClose={closeSettings} />
</RoomProvider>
</SpaceProvider>
</Modal500>

View file

@ -1,80 +0,0 @@
import React from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SettingsSection } from '../../settings/SettingsSection';
import { SectionLabel } from '../../settings/styles.css';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useRoom } from '../../../hooks/useRoom';
import {
RoomProfile,
RoomEncryption,
RoomHistoryVisibility,
RoomJoinRules,
RoomLocalAddresses,
RoomPublishedAddresses,
RoomPublish,
RoomUpgrade,
RoomVoiceMessages,
} from '../../common-settings/general';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type GeneralProps = {
requestClose: () => void;
};
export function General({ requestClose }: GeneralProps) {
const { t } = useTranslation();
const room = useRoom();
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
return (
<Page>
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
{t('RoomSettings.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 permissions={permissions} />
<SettingsSection label={t('RoomSettings.options')}>
<RoomJoinRules permissions={permissions} />
<RoomHistoryVisibility permissions={permissions} />
<RoomEncryption permissions={permissions} />
<RoomVoiceMessages permissions={permissions} />
<RoomPublish permissions={permissions} />
</SettingsSection>
<Box direction="Column" gap="200">
<Text as="span" className={SectionLabel}>
{t('RoomSettings.addresses')}
</Text>
<RoomPublishedAddresses permissions={permissions} />
<RoomLocalAddresses permissions={permissions} />
</Box>
<Box direction="Column" gap="200">
<Text as="span" className={SectionLabel}>
{t('RoomSettings.advanced_options')}
</Text>
<RoomUpgrade permissions={permissions} requestClose={requestClose} />
</Box>
</Box>
</PageContent>
</Scroll>
</Box>
</Page>
);
}

View file

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

View file

@ -1,76 +0,0 @@
import React, { useState } from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom';
import { usePowerLevels } 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';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type PermissionsProps = {
requestClose: () => void;
};
export function Permissions({ requestClose }: PermissionsProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const room = useRoom();
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const permissions = useRoomPermissions(creators, powerLevels);
const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
const permissionGroups = usePermissionGroups(room.isCallRoom());
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>
{t('RoomSettings.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
canEdit={canEditPermissions}
powerLevels={powerLevels}
permissionGroups={permissionGroups}
/>
</Box>
</PageContent>
</Scroll>
</Box>
</Page>
);
}

View file

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

View file

@ -1,230 +0,0 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MessageEvent, StateEvent } from '../../../../types/matrix/room';
import { PermissionGroup } from '../../common-settings/permissions';
export const usePermissionGroups = (isCallRoom: boolean): PermissionGroup[] => {
const { t } = useTranslation();
const groups: PermissionGroup[] = useMemo(() => {
const messagesGroup: PermissionGroup = {
name: t('RoomSettings.perm_messages'),
items: [
{
location: {
key: MessageEvent.RoomMessage,
},
name: t('RoomSettings.perm_send_messages'),
},
{
location: {
key: MessageEvent.Sticker,
},
name: t('RoomSettings.perm_send_stickers'),
},
{
location: {
key: MessageEvent.Reaction,
},
name: t('RoomSettings.perm_send_reactions'),
},
{
location: {
notification: true,
key: 'room',
},
name: t('RoomSettings.perm_ping_room'),
},
{
location: {
state: true,
key: StateEvent.RoomPinnedEvents,
},
name: t('RoomSettings.perm_pin_messages'),
},
{
location: {},
name: t('RoomSettings.perm_other_message_events'),
},
],
};
const callSettingsGroup: PermissionGroup = {
name: t('RoomSettings.perm_calls'),
items: [
{
location: {
state: true,
key: StateEvent.GroupCallMemberPrefix,
},
name: t('RoomSettings.perm_join_call'),
},
],
};
const moderationGroup: PermissionGroup = {
name: t('RoomSettings.perm_moderation'),
items: [
{
location: {
action: true,
key: 'invite',
},
name: t('RoomSettings.perm_invite'),
},
{
location: {
action: true,
key: 'kick',
},
name: t('RoomSettings.perm_kick'),
},
{
location: {
action: true,
key: 'ban',
},
name: t('RoomSettings.perm_ban'),
},
{
location: {
action: true,
key: 'redact',
},
name: t('RoomSettings.perm_delete_others_messages'),
},
{
location: {
key: MessageEvent.RoomRedaction,
},
name: t('RoomSettings.perm_delete_self_messages'),
},
],
};
const roomOverviewGroup: PermissionGroup = {
name: t('RoomSettings.perm_room_overview'),
items: [
{
location: {
state: true,
key: StateEvent.RoomAvatar,
},
name: t('RoomSettings.perm_room_avatar'),
},
{
location: {
state: true,
key: StateEvent.RoomName,
},
name: t('RoomSettings.perm_room_name'),
},
{
location: {
state: true,
key: StateEvent.RoomTopic,
},
name: t('RoomSettings.perm_room_topic'),
},
],
};
const roomSettingsGroup: PermissionGroup = {
name: t('RoomSettings.perm_settings'),
items: [
{
location: {
state: true,
key: StateEvent.RoomJoinRules,
},
name: t('RoomSettings.perm_change_room_access'),
},
{
location: {
state: true,
key: StateEvent.RoomCanonicalAlias,
},
name: t('RoomSettings.perm_publish_address'),
},
{
location: {
state: true,
key: StateEvent.RoomPowerLevels,
},
name: t('RoomSettings.perm_change_all_permission'),
},
{
location: {
state: true,
key: StateEvent.PowerLevelTags,
},
name: t('RoomSettings.perm_edit_power_levels'),
},
{
location: {
state: true,
key: StateEvent.RoomEncryption,
},
name: t('RoomSettings.perm_enable_encryption'),
},
{
location: {
state: true,
key: StateEvent.RoomHistoryVisibility,
},
name: t('RoomSettings.perm_history_visibility'),
},
{
location: {
state: true,
key: StateEvent.RoomTombstone,
},
name: t('RoomSettings.perm_upgrade_room'),
},
{
location: {
state: true,
},
name: t('RoomSettings.perm_other_settings'),
},
],
};
const otherSettingsGroup: PermissionGroup = {
name: t('RoomSettings.perm_other'),
items: [
{
location: {
state: true,
key: StateEvent.PoniesRoomEmotes,
},
name: t('RoomSettings.perm_manage_emojis_stickers'),
},
{
location: {
state: true,
key: StateEvent.RoomServerAcl,
},
name: t('RoomSettings.perm_change_server_acls'),
},
{
location: {
state: true,
key: 'im.vector.modular.widgets',
},
name: t('RoomSettings.perm_modify_widgets'),
},
],
};
return [
messagesGroup,
...(isCallRoom ? [callSettingsGroup] : []),
moderationGroup,
roomOverviewGroup,
roomSettingsGroup,
otherSettingsGroup,
];
}, [isCallRoom, t]);
return groups;
};

View file

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

View file

@ -31,7 +31,6 @@ import { useRoomMemberCount } from '../../hooks/useRoomMemberCount';
import { Presence, useUserPresence } from '../../hooks/useUserPresence';
import { useRoomAvatar, useRoomName } from '../../hooks/useRoomMeta';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
import { useCallMembers, useCallSession } from '../../hooks/useCall';
import { useSwitchOrStartDmCall } from '../../hooks/useSwitchOrStartDmCall';
@ -42,7 +41,6 @@ import {
useRoomMembersSheetState,
} from '../../state/hooks/roomMembersSheet';
import { callEmbedAtom } from '../../state/callEmbed';
import { RoomSettingsPage } from '../../state/roomSettings';
import { StateEvent } from '../../../types/matrix/room';
import {
getMxIdLocalPart,
@ -179,7 +177,6 @@ export function RoomViewHeaderDm({ callView }: { callView?: boolean }) {
const membersSheetState = useRoomMembersSheetState();
const membersSheetOpen = membersSheetState?.roomId === room.roomId;
const parentSpace = useSpaceOptionally();
const openSettings = useOpenRoomSettings();
// The ⋮ overflow opens the RoomActionsMenu in an anchored PopOut — same
// chrome on desktop and mobile.
@ -203,13 +200,6 @@ export function RoomViewHeaderDm({ callView }: { callView?: boolean }) {
openRoomMembersSheet(room.roomId);
}
};
// `callView` keeps the legacy «open Members in Settings» fallback —
// the members side-pane isn't mounted while we're inside the call
// surface, so a separate path is still needed to reach the list.
const handleCallViewMembers = () => {
openSettings(room.roomId, parentSpace?.roomId, RoomSettingsPage.MembersPage);
};
const avatarNode = (
<Avatar size="300">
{isOneOnOne && peerUserId ? (
@ -356,30 +346,9 @@ export function RoomViewHeaderDm({ callView }: { callView?: boolean }) {
<Box shrink="No" alignItems="Center">
{callButtonVisible && <DmCallButton room={room} />}
{/* Member toggle kept only for `callView` (1:1 or group): the
members side-pane / horseshoe isn't mounted under the call
surface, so we still need a way to reach the member list
via Room Settings Members. Non-call group rooms now use
the identity button above (tap on avatar+title) keeps
the gesture symmetrical with 1:1 peer profile. */}
{callView && screenSize === ScreenSize.Desktop && (
<TooltipProvider
position="Bottom"
offset={4}
tooltip={
<Tooltip>
<Text>{t('Room.members')}</Text>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton fill="None" ref={triggerRef} onClick={handleCallViewMembers}>
<Icon size="400" src={Icons.User} />
</IconButton>
)}
</TooltipProvider>
)}
{/* No separate members button in callView CallView renders its
own member list, and the Room-SettingsMembers page this used
to open was removed with the room-settings redesign. */}
<TooltipProvider
position="Bottom"
align="End"

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { roomSettingsAtom, RoomSettingsPage, RoomSettingsState } from '../roomSettings';
import { roomSettingsAtom, RoomSettingsState } from '../roomSettings';
export const useRoomSettingsState = (): RoomSettingsState | undefined => {
const data = useAtomValue(roomSettingsAtom);
@ -19,13 +19,13 @@ export const useCloseRoomSettings = (): CloseCallback => {
return close;
};
type OpenCallback = (roomId: string, space?: string, page?: RoomSettingsPage) => void;
type OpenCallback = (roomId: string, space?: string) => void;
export const useOpenRoomSettings = (): OpenCallback => {
const setSettings = useSetAtom(roomSettingsAtom);
const open: OpenCallback = useCallback(
(roomId, spaceId, page) => {
setSettings({ roomId, spaceId, page });
(roomId, spaceId) => {
setSettings({ roomId, spaceId });
},
[setSettings]
);

View file

@ -1,15 +1,12 @@
import { atom } from 'jotai';
export enum RoomSettingsPage {
GeneralPage,
MembersPage,
PermissionsPage,
EmojisStickersPage,
DeveloperToolsPage,
}
// Room settings are deliberately minimal post-redesign: ONE page (profile,
// access, encryption, voice) — no nav, no sub-pages. Members live in the
// room card (members sheet); the power-level editor, emoji packs, room
// upgrade and developer tools were Matrix-protocol surfaces end users
// never needed in a chat — dropped from rooms (spaces keep their own full
// set in spaceSettings.ts).
export type RoomSettingsState = {
page?: RoomSettingsPage;
roomId: string;
spaceId?: string;
};