refactor(room-settings): collapse room settings into a single Modal500 page, dropping the per-room permissions and developer surfaces
This commit is contained in:
parent
abae93170a
commit
caff65d9ee
15 changed files with 208 additions and 679 deletions
54
src/app/components/Modal500.css.ts
Normal file
54
src/app/components/Modal500.css.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './General';
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './Permissions';
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { config } from 'folds';
|
||||
|
||||
export const SequenceCardStyle = style({
|
||||
padding: config.space.S300,
|
||||
});
|
||||
|
|
@ -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-Settings→Members page this used
|
||||
to open was removed with the room-settings redesign. */}
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue