feat: extended profile fields, push rule editor, server ACL editor

P2-8: Pronouns (m.pronouns) and Timezone (m.tz) fields in Settings →
Account → Profile; saved via MSC4133 PUT /profile/{userId}/{field};
useExtendedProfile hook fetches both in parallel; UserHero displays
pronouns below display name and timezone string below username

P2-11: Full push rule editor in Settings → Notifications below keyword
rules; covers override/room/sender/underride rule kinds; enable/disable
toggle per rule, human-readable labels for built-in rules, delete button
for custom rules, add-rule form for room and sender rules

P2-12: Server ACL viewer/editor in room settings (Server ACL tab);
reads m.room.server_acl state event; allow/deny server lists with
wildcard validation; allow IP literals toggle; power-level gated
(edit requires sufficient PL, otherwise read-only view)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:13:33 -04:00
parent 160db1eaef
commit 51a355fe77
9 changed files with 1157 additions and 43 deletions
+69 -41
View File
@@ -19,6 +19,10 @@ import { useRoom } from '../../hooks/useRoom';
import { DeveloperTools } from '../common-settings/developer-tools';
import { ExportRoomHistory } from './ExportRoomHistory';
import { RoomActivityLog } from './RoomActivityLog';
import { RoomServerACL } from './RoomServerACL';
import { usePowerLevels, readPowerLevel } from '../../hooks/usePowerLevels';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { StateEvent } from '../../../types/matrix/room';
type RoomSettingsMenuItem = {
page: RoomSettingsPage;
@@ -26,47 +30,56 @@ type RoomSettingsMenuItem = {
icon: IconSrc;
};
const useRoomSettingsMenuItems = (): RoomSettingsMenuItem[] =>
useMemo(
() => [
{
page: RoomSettingsPage.GeneralPage,
name: 'General',
icon: Icons.Setting,
},
{
page: RoomSettingsPage.MembersPage,
name: 'Members',
icon: Icons.User,
},
{
page: RoomSettingsPage.PermissionsPage,
name: 'Permissions',
icon: Icons.Lock,
},
{
page: RoomSettingsPage.EmojisStickersPage,
name: 'Emojis & Stickers',
icon: Icons.Smile,
},
{
page: RoomSettingsPage.DeveloperToolsPage,
name: 'Developer Tools',
icon: Icons.Terminal,
},
{
page: RoomSettingsPage.ExportPage,
name: 'Export',
icon: Icons.Download,
},
{
page: RoomSettingsPage.ActivityLogPage,
name: 'Activity',
icon: Icons.RecentClock,
},
],
[],
const BASE_MENU_ITEMS: RoomSettingsMenuItem[] = [
{
page: RoomSettingsPage.GeneralPage,
name: 'General',
icon: Icons.Setting,
},
{
page: RoomSettingsPage.MembersPage,
name: 'Members',
icon: Icons.User,
},
{
page: RoomSettingsPage.PermissionsPage,
name: 'Permissions',
icon: Icons.Lock,
},
{
page: RoomSettingsPage.EmojisStickersPage,
name: 'Emojis & Stickers',
icon: Icons.Smile,
},
{
page: RoomSettingsPage.DeveloperToolsPage,
name: 'Developer Tools',
icon: Icons.Terminal,
},
{
page: RoomSettingsPage.ExportPage,
name: 'Export',
icon: Icons.Download,
},
{
page: RoomSettingsPage.ActivityLogPage,
name: 'Activity',
icon: Icons.RecentClock,
},
];
const SERVER_ACL_MENU_ITEM: RoomSettingsMenuItem = {
page: RoomSettingsPage.ServerACLPage,
name: 'Server ACL',
icon: Icons.Shield,
};
function useRoomSettingsMenuItems(canSeeServerACL: boolean): RoomSettingsMenuItem[] {
return useMemo(
() => (canSeeServerACL ? [...BASE_MENU_ITEMS, SERVER_ACL_MENU_ITEM] : BASE_MENU_ITEMS),
[canSeeServerACL],
);
}
type RoomSettingsProps = {
initialPage?: RoomSettingsPage;
@@ -86,12 +99,24 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
? (mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined)
: undefined;
// Power level check: show Server ACL menu item to anyone who can read the state
// (i.e. has at least state_default power level, or a custom ACL event power).
// We show it to all users at or above the required power level; read-only view
// for those who cannot edit.
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const myUserId = mx.getSafeUserId();
const myPL = readPowerLevel.user(powerLevels, myUserId);
const requiredPL = readPowerLevel.state(powerLevels, StateEvent.RoomServerAcl);
// Show the menu item if user meets the power level OR is a room creator.
const canSeeServerACL = myPL >= requiredPL || creators.has(myUserId);
const screenSize = useScreenSizeContext();
const [activePage, setActivePage] = useState<RoomSettingsPage | undefined>(() => {
if (initialPage) return initialPage;
return screenSize === ScreenSize.Mobile ? undefined : RoomSettingsPage.GeneralPage;
});
const menuItems = useRoomSettingsMenuItems();
const menuItems = useRoomSettingsMenuItems(canSeeServerACL);
const handlePageRequestClose = () => {
if (screenSize === ScreenSize.Mobile) {
@@ -190,6 +215,9 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
{activePage === RoomSettingsPage.ActivityLogPage && (
<RoomActivityLog requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.ServerACLPage && (
<RoomServerACL requestClose={handlePageRequestClose} />
)}
</PageRoot>
);
}