imnyang/cinny
Watch
1
0
Fork
You've already forked cinny
0

Support room version 12 (#2399)

* WIP - support room version 12

* add room creators hook

* revert changes from powerlevels

* improve use room creators hook

* add hook to get dm users

* add options to add creators in create room/space

* add member item component in member drawer

* remove unused import

* extract member drawer header component

* get room creators as set only if room version support them

* add room permissions hook

* support room v12 creators power

* make predecessor event id optional

* add info about founders in permissions

* allow to create infinite powers to room creators

* allow everyone with permission to create infinite power

* handle additional creators in room upgrade

* add option to follow space tombstone
This commit is contained in:
Ajay Bura 2025-08-12 19:42:30 +05:30 • committed by GitHub
commit f82cfead46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 1715 additions and 781 deletions

View file

@ -0,0 +1,306 @@
import {
Box,
Button,
Chip,
config,
Icon,
Icons,
Input,
Line,
Menu,
MenuItem,
PopOut,
RectCords,
Scroll,
Text,
toRem,
} from 'folds';
import { isKeyHotkey } from 'is-hotkey';
import FocusTrap from 'focus-trap-react';
import React, {
ChangeEventHandler,
KeyboardEventHandler,
MouseEventHandler,
useMemo,
useState,
} from 'react';
import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix';
import { useDirectUsers } from '../../hooks/useDirectUsers';
import { SettingTile } from '../setting-tile';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { stopPropagation } from '../../utils/keyboard';
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
import { findAndReplace } from '../../utils/findAndReplace';
import { highlightText } from '../../styles/CustomHtml.css';
import { makeHighlightRegex } from '../../plugins/react-custom-html-parser';
export const useAdditionalCreators = (defaultCreators?: string[]) => {
const mx = useMatrixClient();
const [additionalCreators, setAdditionalCreators] = useState<string[]>(
() => defaultCreators?.filter((id) => id !== mx.getSafeUserId()) ?? []
);
const addAdditionalCreator = (userId: string) => {
if (userId === mx.getSafeUserId()) return;
setAdditionalCreators((creators) => {
const creatorsSet = new Set(creators);
creatorsSet.add(userId);
return Array.from(creatorsSet);
});
};
const removeAdditionalCreator = (userId: string) => {
setAdditionalCreators((creators) => {
const creatorsSet = new Set(creators);
creatorsSet.delete(userId);
return Array.from(creatorsSet);
});
};
return {
additionalCreators,
addAdditionalCreator,
removeAdditionalCreator,
};
};
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
matchOptions: {
contain: true,
},
};
const getUserIdString = (userId: string) => getMxIdLocalPart(userId) ?? userId;
type AdditionalCreatorInputProps = {
additionalCreators: string[];
onSelect: (userId: string) => void;
onRemove: (userId: string) => void;
disabled?: boolean;
};
export function AdditionalCreatorInput({
additionalCreators,
onSelect,
onRemove,
disabled,
}: AdditionalCreatorInputProps) {
const mx = useMatrixClient();
const [menuCords, setMenuCords] = useState<RectCords>();
const directUsers = useDirectUsers();
const [validUserId, setValidUserId] = useState<string>();
const filteredUsers = useMemo(
() => directUsers.filter((userId) => !additionalCreators.includes(userId)),
[directUsers, additionalCreators]
);
const [result, search, resetSearch] = useAsyncSearch(
filteredUsers,
getUserIdString,
SEARCH_OPTIONS
);
const queryHighlighRegex = result?.query ? makeHighlightRegex([result.query]) : undefined;
const suggestionUsers = result
? result.items
: filteredUsers.sort((a, b) => (a.toLocaleLowerCase() >= b.toLocaleLowerCase() ? 1 : -1));
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuCords(evt.currentTarget.getBoundingClientRect());
};
const handleCloseMenu = () => {
setMenuCords(undefined);
setValidUserId(undefined);
resetSearch();
};
const handleCreatorChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
const creatorInput = evt.currentTarget;
const creator = creatorInput.value.trim();
if (isUserId(creator)) {
setValidUserId(creator);
} else {
setValidUserId(undefined);
const term =
getMxIdLocalPart(creator) ?? (creator.startsWith('@') ? creator.slice(1) : creator);
if (term) {
search(term);
} else {
resetSearch();
}
}
};
const handleSelectUserId = (userId?: string) => {
if (userId && isUserId(userId)) {
onSelect(userId);
handleCloseMenu();
}
};
const handleCreatorKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
if (isKeyHotkey('enter', evt)) {
evt.preventDefault();
const creator = evt.currentTarget.value.trim();
handleSelectUserId(isUserId(creator) ? creator : suggestionUsers[0]);
}
};
const handleEnterClick = () => {
handleSelectUserId(validUserId);
};
return (
<SettingTile
title="Founders"
description="Special privileged users can be assigned during creation. These users have elevated control and can only be modified during a upgrade."
>
<Box shrink="No" direction="Column" gap="100">
<Box gap="200" wrap="Wrap">
<Chip type="button" variant="Primary" radii="Pill" outlined>
<Text size="B300">{mx.getSafeUserId()}</Text>
</Chip>
{additionalCreators.map((creator) => (
<Chip
type="button"
key={creator}
variant="Secondary"
radii="Pill"
after={<Icon size="50" src={Icons.Cross} />}
onClick={() => onRemove(creator)}
disabled={disabled}
>
<Text size="B300">{creator}</Text>
</Chip>
))}
<PopOut
anchor={menuCords}
position="Bottom"
align="Center"
content={
<FocusTrap
focusTrapOptions={{
onDeactivate: handleCloseMenu,
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu
style={{
width: '100vw',
maxWidth: toRem(300),
height: toRem(250),
display: 'flex',
}}
>
<Box grow="Yes" direction="Column">
<Box shrink="No" gap="100" style={{ padding: config.space.S100 }}>
<Box grow="Yes" direction="Column" gap="100">
<Input
size="400"
variant="Background"
radii="300"
outlined
placeholder="@john:server"
onChange={handleCreatorChange}
onKeyDown={handleCreatorKeyDown}
/>
</Box>
<Button
type="button"
variant="Success"
radii="300"
onClick={handleEnterClick}
disabled={!validUserId}
>
<Text size="B400">Enter</Text>
</Button>
</Box>
<Line size="300" />
<Box grow="Yes" direction="Column">
{!validUserId && suggestionUsers.length > 0 ? (
<Scroll size="300" hideTrack>
<Box
grow="Yes"
direction="Column"
gap="100"
style={{ padding: config.space.S200, paddingRight: 0 }}
>
{suggestionUsers.map((userId) => (
<MenuItem
key={userId}
size="300"
variant="Surface"
radii="300"
onClick={() => handleSelectUserId(userId)}
after={
<Text size="T200" truncate>
{getMxIdServer(userId)}
</Text>
}
>
<Box grow="Yes">
<Text size="T200" truncate>
<b>
{queryHighlighRegex
? findAndReplace(
getMxIdLocalPart(userId) ?? userId,
queryHighlighRegex,
(match, pushIndex) => (
<span
key={`highlight-${pushIndex}`}
className={highlightText}
>
{match[0]}
</span>
),
(txt) => txt
)
: getMxIdLocalPart(userId)}
</b>
</Text>
</Box>
</MenuItem>
))}
</Box>
</Scroll>
) : (
<Box
grow="Yes"
alignItems="Center"
justifyContent="Center"
direction="Column"
gap="100"
>
<Text size="H6" align="Center">
No Suggestions
</Text>
<Text size="T200" align="Center">
Please provide the user ID and hit Enter.
</Text>
</Box>
)}
</Box>
</Box>
</Menu>
</FocusTrap>
}
>
<Chip
type="button"
variant="Secondary"
radii="Pill"
onClick={handleOpenMenu}
aria-pressed={!!menuCords}
disabled={disabled}
>
<Icon size="50" src={Icons.Plus} />
</Chip>
</PopOut>
</Box>
</Box>
</SettingTile>
);
}

View file

@ -47,7 +47,7 @@ export function RoomVersionSelector({
gap="500" gap="500"
> >
<SettingTile <SettingTile
title="Room Version" title="Version"
after={ after={
<PopOut <PopOut
anchor={menuCords} anchor={menuCords}

View file

@ -2,3 +2,4 @@ export * from './CreateRoomKindSelector';
export * from './CreateRoomAliasInput'; export * from './CreateRoomAliasInput';
export * from './RoomVersionSelector'; export * from './RoomVersionSelector';
export * from './utils'; export * from './utils';
export * from './AdditionalCreatorInput';

View file

@ -14,7 +14,8 @@ import { getMxIdServer } from '../../utils/matrix';
export const createRoomCreationContent = ( export const createRoomCreationContent = (
type: RoomType | undefined, type: RoomType | undefined,
allowFederation: boolean allowFederation: boolean,
additionalCreators: string[] | undefined
): object => { ): object => {
const content: Record<string, any> = {}; const content: Record<string, any> = {};
if (typeof type === 'string') { if (typeof type === 'string') {
@ -23,6 +24,9 @@ export const createRoomCreationContent = (
if (allowFederation === false) { if (allowFederation === false) {
content['m.federate'] = false; content['m.federate'] = false;
} }
if (Array.isArray(additionalCreators)) {
content.additional_creators = additionalCreators;
}
return content; return content;
}; };
@ -89,6 +93,7 @@ export type CreateRoomData = {
encryption?: boolean; encryption?: boolean;
knock: boolean; knock: boolean;
allowFederation: boolean; allowFederation: boolean;
additionalCreators?: string[];
}; };
export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise<string> => { export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise<string> => {
const initialState: ICreateRoomStateEvent[] = []; const initialState: ICreateRoomStateEvent[] = [];
@ -108,7 +113,11 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
name: data.name, name: data.name,
topic: data.topic, topic: data.topic,
room_alias_name: data.aliasLocalPart, room_alias_name: data.aliasLocalPart,
creation_content: createRoomCreationContent(data.type, data.allowFederation), creation_content: createRoomCreationContent(
data.type,
data.allowFederation,
data.additionalCreators
),
initial_state: initialState, initial_state: initialState,
}; };

View file

@ -1,12 +1,14 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { usePowerLevels } from '../../hooks/usePowerLevels';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { ImagePackContent } from './ImagePackContent'; import { ImagePackContent } from './ImagePackContent';
import { ImagePack, PackContent } from '../../plugins/custom-emoji'; import { ImagePack, PackContent } from '../../plugins/custom-emoji';
import { StateEvent } from '../../../types/matrix/room'; import { StateEvent } from '../../../types/matrix/room';
import { useRoomImagePack } from '../../hooks/useImagePacks'; import { useRoomImagePack } from '../../hooks/useImagePacks';
import { randomStr } from '../../utils/common'; import { randomStr } from '../../utils/common';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useRoomCreators } from '../../hooks/useRoomCreators';
type RoomImagePackProps = { type RoomImagePackProps = {
room: Room; room: Room;
@ -17,9 +19,10 @@ export function RoomImagePack({ room, stateKey }: RoomImagePackProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const userId = mx.getUserId()!; const userId = mx.getUserId()!;
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); const permissions = useRoomPermissions(creators, powerLevels);
const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(userId)); const canEditImagePack = permissions.stateEvent(StateEvent.PoniesRoomEmotes, userId);
const fallbackPack = useMemo(() => { const fallbackPack = useMemo(() => {
const fakePackId = randomStr(4); const fakePackId = randomStr(4);

View file

@ -10,8 +10,8 @@ import * as css from './Reply.css';
import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content'; import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content';
import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser'; import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
import { useRoomEvent } from '../../hooks/useRoomEvent'; import { useRoomEvent } from '../../hooks/useRoomEvent';
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import { GetMemberPowerTag } from '../../hooks/useMemberPowerTag';
type ReplyLayoutProps = { type ReplyLayoutProps = {
userColor?: string; userColor?: string;
@ -57,8 +57,7 @@ type ReplyProps = {
replyEventId: string; replyEventId: string;
threadRootId?: string | undefined; threadRootId?: string | undefined;
onClick?: MouseEventHandler | undefined; onClick?: MouseEventHandler | undefined;
getPowerLevel?: (userId: string) => number; getMemberPowerTag?: GetMemberPowerTag;
getPowerLevelTag?: GetPowerLevelTag;
accessibleTagColors?: Map<string, string>; accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean; legacyUsernameColor?: boolean;
}; };
@ -71,8 +70,7 @@ export const Reply = as<'div', ReplyProps>(
replyEventId, replyEventId,
threadRootId, threadRootId,
onClick, onClick,
getPowerLevel, getMemberPowerTag,
getPowerLevelTag,
accessibleTagColors, accessibleTagColors,
legacyUsernameColor, legacyUsernameColor,
...props ...props
@ -88,8 +86,7 @@ export const Reply = as<'div', ReplyProps>(
const { body } = replyEvent?.getContent() ?? {}; const { body } = replyEvent?.getContent() ?? {};
const sender = replyEvent?.getSender(); const sender = replyEvent?.getSender();
const senderPL = sender && getPowerLevel?.(sender); const powerTag = sender ? getMemberPowerTag?.(sender) : undefined;
const powerTag = typeof senderPL === 'number' ? getPowerLevelTag?.(senderPL) : undefined;
const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined; const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined;
const usernameColor = legacyUsernameColor ? colorMXID(sender ?? replyEventId) : tagColor; const usernameColor = legacyUsernameColor ? colorMXID(sender ?? replyEventId) : tagColor;

View file

@ -87,7 +87,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
{typeof prevRoomId === 'string' && {typeof prevRoomId === 'string' &&
(mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? ( (mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (
<Button <Button
onClick={() => navigateRoom(prevRoomId)} onClick={() => navigateRoom(prevRoomId, createContent?.predecessor?.event_id)}
variant="Success" variant="Success"
size="300" size="300"
fill="Soft" fill="Soft"

View file

@ -0,0 +1,101 @@
import { Chip, config, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text } from 'folds';
import React, { MouseEventHandler, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import { isKeyHotkey } from 'is-hotkey';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
import { PowerColorBadge, PowerIcon } from '../power';
import { getPowerTagIconSrc } from '../../hooks/useMemberPowerTag';
import { useMatrixClient } from '../../hooks/useMatrixClient';
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';
export function CreatorChip() {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const room = useRoom();
const space = useSpaceOptionally();
const openRoomSettings = useOpenRoomSettings();
const openSpaceSettings = useOpenSpaceSettings();
const [cords, setCords] = useState<RectCords>();
const tag = useRoomCreatorsTag();
const tagIconSrc = tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon);
const open: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};
const close = () => setCords(undefined);
return (
<PopOut
anchor={cords}
position="Bottom"
align="Start"
offset={4}
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: close,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt),
isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt),
}}
>
<Menu>
<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>
</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>
</PopOut>
);
}

View file

@ -26,8 +26,8 @@ import { isKeyHotkey } from 'is-hotkey';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { PowerColorBadge, PowerIcon } from '../power'; import { PowerColorBadge, PowerIcon } from '../power';
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { useGetMemberPowerLevel, usePowerLevels } from '../../hooks/usePowerLevels';
import { getPowers, getTagIconSrc, usePowerLevelTags } from '../../hooks/usePowerLevelTags'; import { getPowers, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { stopPropagation } from '../../utils/keyboard'; import { stopPropagation } from '../../utils/keyboard';
import { StateEvent } from '../../../types/matrix/room'; import { StateEvent } from '../../../types/matrix/room';
import { useOpenRoomSettings } from '../../state/hooks/roomSettings'; import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
@ -39,6 +39,10 @@ import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
import { SpaceSettingsPage } from '../../state/spaceSettings'; import { SpaceSettingsPage } from '../../state/spaceSettings';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { BreakWord } from '../../styles/Text.css'; import { BreakWord } from '../../styles/Text.css';
import { getPowerTagIconSrc, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare';
type SelfDemoteAlertProps = { type SelfDemoteAlertProps = {
power: number; power: number;
@ -149,16 +153,22 @@ export function PowerChip({ userId }: { userId: string }) {
const openSpaceSettings = useOpenSpaceSettings(); const openSpaceSettings = useOpenSpaceSettings();
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); const creators = useRoomCreators(room);
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
const myPower = getPowerLevel(mx.getSafeUserId());
const userPower = getPowerLevel(userId);
const canChangePowers =
canSendStateEvent(StateEvent.RoomPowerLevels, myPower) &&
(mx.getSafeUserId() === userId ? true : myPower > userPower);
const tag = getPowerLevelTag(userPower); const permissions = useRoomPermissions(creators, powerLevels);
const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon); const getMemberPowerLevel = useGetMemberPowerLevel(powerLevels);
const { hasMorePower } = useMemberPowerCompare(creators, powerLevels);
const powerLevelTags = usePowerLevelTags(room, powerLevels);
const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
const myUserId = mx.getSafeUserId();
const canChangePowers =
permissions.stateEvent(StateEvent.RoomPowerLevels, myUserId) &&
(myUserId === userId ? true : hasMorePower(myUserId, userId));
const tag = getMemberPowerTag(userId);
const tagIconSrc = tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon);
const [cords, setCords] = useState<RectCords>(); const [cords, setCords] = useState<RectCords>();
@ -184,13 +194,13 @@ export function PowerChip({ userId }: { userId: string }) {
const handlePowerSelect = (power: number): void => { const handlePowerSelect = (power: number): void => {
close(); close();
if (!canChangePowers) return; if (!canChangePowers) return;
if (power === userPower) return; if (power === getMemberPowerLevel(userId)) return;
if (userId === mx.getSafeUserId()) { if (userId === mx.getSafeUserId()) {
setSelfDemote(power); setSelfDemote(power);
return; return;
} }
if (power === myPower) { if (!creators.has(myUserId) && power === getMemberPowerLevel(myUserId)) {
setSharedPower(power); setSharedPower(power);
return; return;
} }
@ -242,19 +252,22 @@ export function PowerChip({ userId }: { userId: string }) {
{getPowers(powerLevelTags).map((power) => { {getPowers(powerLevelTags).map((power) => {
const powerTag = powerLevelTags[power]; const powerTag = powerLevelTags[power];
const powerTagIconSrc = const powerTagIconSrc =
powerTag.icon && getTagIconSrc(mx, useAuthentication, powerTag.icon); powerTag.icon && getPowerTagIconSrc(mx, useAuthentication, powerTag.icon);
const canAssignPower = power <= myPower; const selected = getMemberPowerLevel(userId) === power;
const canAssignPower = creators.has(myUserId)
? true
: power <= getMemberPowerLevel(myUserId);
return ( return (
<MenuItem <MenuItem
key={power} key={power}
variant={userPower === power ? 'Primary' : 'Surface'} variant={selected ? 'Primary' : 'Surface'}
fill="None" fill="None"
size="300" size="300"
radii="300" radii="300"
aria-disabled={changing || !canChangePowers || !canAssignPower} aria-disabled={changing || !canChangePowers || !canAssignPower}
aria-pressed={userPower === power} aria-pressed={selected}
before={<PowerColorBadge color={powerTag.color} />} before={<PowerColorBadge color={powerTag.color} />}
after={ after={
powerTagIconSrc ? ( powerTagIconSrc ? (

View file

@ -5,12 +5,12 @@ import { getDMRoomFor, getMxIdServer, mxcUrlToHttp } from '../../utils/matrix';
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room'; import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { usePowerLevels } from '../../hooks/usePowerLevels';
import { useRoom } from '../../hooks/useRoom'; import { useRoom } from '../../hooks/useRoom';
import { useUserPresence } from '../../hooks/useUserPresence'; import { useUserPresence } from '../../hooks/useUserPresence';
import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { createDM, ignore } from '../../../client/action/room'; import { createDM } from '../../../client/action/room';
import { hasDevices } from '../../../util/matrixUtil'; import { hasDevices } from '../../../util/matrixUtil';
import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useAlive } from '../../hooks/useAlive'; import { useAlive } from '../../hooks/useAlive';
@ -20,6 +20,10 @@ import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers'; import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
import { useMembership } from '../../hooks/useMembership'; import { useMembership } from '../../hooks/useMembership';
import { Membership } from '../../../types/matrix/room'; import { Membership } from '../../../types/matrix/room';
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare';
import { CreatorChip } from './CreatorChip';
type UserRoomProfileProps = { type UserRoomProfileProps = {
userId: string; userId: string;
@ -34,13 +38,19 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
const ignored = ignoredUsers.includes(userId); const ignored = ignoredUsers.includes(userId);
const room = useRoom(); const room = useRoom();
const powerlevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerlevels); const creators = useRoomCreators(room);
const myPowerLevel = getPowerLevel(mx.getSafeUserId());
const userPowerLevel = getPowerLevel(userId); const permissions = useRoomPermissions(creators, powerLevels);
const canKick = canDoAction('kick', myPowerLevel) && myPowerLevel > userPowerLevel; const { hasMorePower } = useMemberPowerCompare(creators, powerLevels);
const canBan = canDoAction('ban', myPowerLevel) && myPowerLevel > userPowerLevel;
const canInvite = canDoAction('invite', myPowerLevel); const myUserId = mx.getSafeUserId();
const creator = creators.has(userId);
const canKickUser = permissions.action('kick', myUserId) && hasMorePower(myUserId, userId);
const canBanUser = permissions.action('ban', myUserId) && hasMorePower(myUserId, userId);
const canUnban = permissions.action('ban', myUserId);
const canInvite = permissions.action('invite', myUserId);
const member = room.getMember(userId); const member = room.getMember(userId);
const membership = useMembership(room, userId); const membership = useMembership(room, userId);
@ -113,7 +123,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
<Box alignItems="Center" gap="200" wrap="Wrap"> <Box alignItems="Center" gap="200" wrap="Wrap">
{server && <ServerChip server={server} />} {server && <ServerChip server={server} />}
<ShareChip userId={userId} /> <ShareChip userId={userId} />
<PowerChip userId={userId} /> {creator ? <CreatorChip /> : <PowerChip userId={userId} />}
<MutualRoomsChip userId={userId} /> <MutualRoomsChip userId={userId} />
<OptionsChip userId={userId} /> <OptionsChip userId={userId} />
</Box> </Box>
@ -123,7 +133,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
<UserBanAlert <UserBanAlert
userId={userId} userId={userId}
reason={member.events.member?.getContent().reason} reason={member.events.member?.getContent().reason}
canUnban={canBan} canUnban={canUnban}
bannedBy={member.events.member?.getSender()} bannedBy={member.events.member?.getSender()}
ts={member.events.member?.getTs()} ts={member.events.member?.getTs()}
/> />
@ -142,7 +152,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
<UserInviteAlert <UserInviteAlert
userId={userId} userId={userId}
reason={member.events.member?.getContent().reason} reason={member.events.member?.getContent().reason}
canKick={canKick} canKick={canKickUser}
invitedBy={member.events.member?.getSender()} invitedBy={member.events.member?.getSender()}
ts={member.events.member?.getTs()} ts={member.events.member?.getTs()}
/> />
@ -150,8 +160,8 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
<UserModeration <UserModeration
userId={userId} userId={userId}
canInvite={canInvite && membership === Membership.Leave} canInvite={canInvite && membership === Membership.Leave}
canKick={canKick && membership === Membership.Join} canKick={canKickUser && membership === Membership.Join}
canBan={canBan && membership !== Membership.Ban} canBan={canBanUser && membership !== Membership.Ban}
/> />
</Box> </Box>
</Box> </Box>

View file

@ -27,8 +27,10 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { syntaxErrorPosition } from '../../../utils/dom'; import { syntaxErrorPosition } from '../../../utils/dom';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';
import { SequenceCardStyle } from '../styles.css'; import { SequenceCardStyle } from '../styles.css';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useTextAreaCodeEditor } from '../../../hooks/useTextAreaCodeEditor'; import { useTextAreaCodeEditor } from '../../../hooks/useTextAreaCodeEditor';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
const EDITOR_INTENT_SPACE_COUNT = 2; const EDITOR_INTENT_SPACE_COUNT = 2;
@ -244,8 +246,10 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi
const stateEvent = useStateEvent(room, type as unknown as StateEvent, stateKey); const stateEvent = useStateEvent(room, type as unknown as StateEvent, stateKey);
const [editContent, setEditContent] = useState<object>(); const [editContent, setEditContent] = useState<object>();
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels); const creators = useRoomCreators(room);
const canEdit = canSendStateEvent(type, getPowerLevel(mx.getSafeUserId()));
const permissions = useRoomPermissions(creators, powerLevels);
const canEdit = permissions.stateEvent(type, mx.getSafeUserId());
const eventJSONStr = useMemo(() => { const eventJSONStr = useMemo(() => {
if (!stateEvent) return ''; if (!stateEvent) return '';

View file

@ -33,11 +33,13 @@ import { SequenceCardStyle } from '../styles.css';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { mxcUrlToHttp } from '../../../utils/matrix'; import { mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { StateEvent } from '../../../../types/matrix/room'; import { StateEvent } from '../../../../types/matrix/room';
import { suffixRename } from '../../../utils/common'; import { suffixRename } from '../../../utils/common';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useAlive } from '../../../hooks/useAlive'; import { useAlive } from '../../../hooks/useAlive';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type CreatePackTileProps = { type CreatePackTileProps = {
packs: ImagePack[]; packs: ImagePack[];
@ -146,8 +148,10 @@ export function RoomPacks({ onViewPack }: RoomPacksProps) {
const alive = useAlive(); const alive = useAlive();
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { canSendStateEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels); const creators = useRoomCreators(room);
const canEdit = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(mx.getSafeUserId()));
const permissions = useRoomPermissions(creators, powerLevels);
const canEdit = permissions.stateEvent(StateEvent.PoniesRoomEmotes, mx.getSafeUserId());
const unfilteredPacks = useRoomImagePacks(room); const unfilteredPacks = useRoomImagePacks(room);
const packs = useMemo(() => unfilteredPacks.filter((pack) => !pack.deleted), [unfilteredPacks]); const packs = useMemo(() => unfilteredPacks.filter((pack) => !pack.deleted), [unfilteredPacks]);

View file

@ -15,7 +15,6 @@ import {
toRem, toRem,
} from 'folds'; } from 'folds';
import { MatrixError } from 'matrix-js-sdk'; import { MatrixError } from 'matrix-js-sdk';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SequenceCardStyle } from '../../room-settings/styles.css';
@ -33,19 +32,19 @@ import { getIdServer } from '../../../../util/matrixUtil';
import { replaceSpaceWithDash } from '../../../utils/common'; import { replaceSpaceWithDash } from '../../../utils/common';
import { useAlive } from '../../../hooks/useAlive'; import { useAlive } from '../../../hooks/useAlive';
import { StateEvent } from '../../../../types/matrix/room'; import { StateEvent } from '../../../../types/matrix/room';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
type RoomPublishedAddressesProps = { type RoomPublishedAddressesProps = {
powerLevels: IPowerLevels; permissions: RoomPermissionsAPI;
}; };
export function RoomPublishedAddresses({ powerLevels }: RoomPublishedAddressesProps) { export function RoomPublishedAddresses({ permissions }: RoomPublishedAddressesProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
const canEditCanonical = powerLevelAPI.canSendStateEvent( const canEditCanonical = permissions.stateEvent(
powerLevels,
StateEvent.RoomCanonicalAlias, StateEvent.RoomCanonicalAlias,
userPowerLevel mx.getSafeUserId()
); );
const [canonicalAlias, publishedAliases] = usePublishedAliases(room); const [canonicalAlias, publishedAliases] = usePublishedAliases(room);
@ -360,14 +359,13 @@ function LocalAddressesList({
); );
} }
export function RoomLocalAddresses({ powerLevels }: { powerLevels: IPowerLevels }) { export function RoomLocalAddresses({ permissions }: { permissions: RoomPermissionsAPI }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
const canEditCanonical = powerLevelAPI.canSendStateEvent( const canEditCanonical = permissions.stateEvent(
powerLevels,
StateEvent.RoomCanonicalAlias, StateEvent.RoomCanonicalAlias,
userPowerLevel mx.getSafeUserId()
); );
const [expand, setExpand] = useState(false); const [expand, setExpand] = useState(false);

View file

@ -21,28 +21,24 @@ import FocusTrap from 'focus-trap-react';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { StateEvent } from '../../../../types/matrix/room'; import { StateEvent } from '../../../../types/matrix/room';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useRoom } from '../../../hooks/useRoom'; import { useRoom } from '../../../hooks/useRoom';
import { useStateEvent } from '../../../hooks/useStateEvent'; import { useStateEvent } from '../../../hooks/useStateEvent';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
const ROOM_ENC_ALGO = 'm.megolm.v1.aes-sha2'; const ROOM_ENC_ALGO = 'm.megolm.v1.aes-sha2';
type RoomEncryptionProps = { type RoomEncryptionProps = {
powerLevels: IPowerLevels; permissions: RoomPermissionsAPI;
}; };
export function RoomEncryption({ powerLevels }: RoomEncryptionProps) { export function RoomEncryption({ permissions }: RoomEncryptionProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
const canEnable = powerLevelAPI.canSendStateEvent( const canEnable = permissions.stateEvent(StateEvent.RoomEncryption, mx.getSafeUserId());
powerLevels,
StateEvent.RoomEncryption,
userPowerLevel
);
const content = useStateEvent(room, StateEvent.RoomEncryption)?.getContent<{ const content = useStateEvent(room, StateEvent.RoomEncryption)?.getContent<{
algorithm: string; algorithm: string;
}>(); }>();

View file

@ -18,13 +18,13 @@ import FocusTrap from 'focus-trap-react';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRoom } from '../../../hooks/useRoom'; import { useRoom } from '../../../hooks/useRoom';
import { StateEvent } from '../../../../types/matrix/room'; import { StateEvent } from '../../../../types/matrix/room';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useStateEvent } from '../../../hooks/useStateEvent'; import { useStateEvent } from '../../../hooks/useStateEvent';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
const useVisibilityStr = () => const useVisibilityStr = () =>
useMemo( useMemo(
@ -49,17 +49,13 @@ const useVisibilityMenu = () =>
); );
type RoomHistoryVisibilityProps = { type RoomHistoryVisibilityProps = {
powerLevels: IPowerLevels; permissions: RoomPermissionsAPI;
}; };
export function RoomHistoryVisibility({ powerLevels }: RoomHistoryVisibilityProps) { export function RoomHistoryVisibility({ permissions }: RoomHistoryVisibilityProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
const canEdit = powerLevelAPI.canSendStateEvent( const canEdit = permissions.stateEvent(StateEvent.RoomHistoryVisibility, mx.getSafeUserId());
powerLevels,
StateEvent.RoomHistoryVisibility,
userPowerLevel
);
const visibilityEvent = useStateEvent(room, StateEvent.RoomHistoryVisibility); const visibilityEvent = useStateEvent(room, StateEvent.RoomHistoryVisibility);
const historyVisibility: HistoryVisibility = const historyVisibility: HistoryVisibility =

View file

@ -3,7 +3,6 @@ import { color, Text } from 'folds';
import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk'; import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { import {
ExtendedJoinRules, ExtendedJoinRules,
JoinRulesSwitcher, JoinRulesSwitcher,
@ -32,6 +31,7 @@ import {
knockSupported, knockSupported,
restrictedSupported, restrictedSupported,
} from '../../../utils/matrix'; } from '../../../utils/matrix';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
type RestrictedRoomAllowContent = { type RestrictedRoomAllowContent = {
room_id: string; room_id: string;
@ -39,9 +39,9 @@ type RestrictedRoomAllowContent = {
}; };
type RoomJoinRulesProps = { type RoomJoinRulesProps = {
powerLevels: IPowerLevels; permissions: RoomPermissionsAPI;
}; };
export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) { export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const allowKnockRestricted = knockRestrictedSupported(room.getVersion()); const allowKnockRestricted = knockRestrictedSupported(room.getVersion());
@ -53,12 +53,7 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
const subspacesScope = useRecursiveChildSpaceScopeFactory(mx, roomIdToParents); const subspacesScope = useRecursiveChildSpaceScopeFactory(mx, roomIdToParents);
const subspaces = useSpaceChildren(allRoomsAtom, space?.roomId ?? '', subspacesScope); const subspaces = useSpaceChildren(allRoomsAtom, space?.roomId ?? '', subspacesScope);
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); const canEdit = permissions.stateEvent(StateEvent.RoomHistoryVisibility, mx.getSafeUserId());
const canEdit = powerLevelAPI.canSendStateEvent(
powerLevels,
StateEvent.RoomHistoryVisibility,
userPowerLevel
);
const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules); const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules);
const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>(); const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>();

View file

@ -32,7 +32,6 @@ import { RoomAvatar, RoomIcon } from '../../../components/room-avatar';
import { mxcUrlToHttp } from '../../../utils/matrix'; import { mxcUrlToHttp } from '../../../utils/matrix';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { IPowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import { StateEvent } from '../../../../types/matrix/room'; import { StateEvent } from '../../../../types/matrix/room';
import { CompactUploadCardRenderer } from '../../../components/upload-card'; import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { useObjectURL } from '../../../hooks/useObjectURL'; import { useObjectURL } from '../../../hooks/useObjectURL';
@ -40,6 +39,7 @@ import { createUploadAtom, UploadSuccess } from '../../../state/upload';
import { useFilePicker } from '../../../hooks/useFilePicker'; import { useFilePicker } from '../../../hooks/useFilePicker';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useAlive } from '../../../hooks/useAlive'; import { useAlive } from '../../../hooks/useAlive';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
type RoomProfileEditProps = { type RoomProfileEditProps = {
canEditAvatar: boolean; canEditAvatar: boolean;
@ -261,24 +261,22 @@ export function RoomProfileEdit({
} }
type RoomProfileProps = { type RoomProfileProps = {
powerLevels: IPowerLevels; permissions: RoomPermissionsAPI;
}; };
export function RoomProfile({ powerLevels }: RoomProfileProps) { export function RoomProfile({ permissions }: RoomProfileProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const room = useRoom(); const room = useRoom();
const directs = useAtomValue(mDirectAtom); const directs = useAtomValue(mDirectAtom);
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
const userPowerLevel = getPowerLevel(mx.getSafeUserId());
const avatar = useRoomAvatar(room, directs.has(room.roomId)); const avatar = useRoomAvatar(room, directs.has(room.roomId));
const name = useRoomName(room); const name = useRoomName(room);
const topic = useRoomTopic(room); const topic = useRoomTopic(room);
const joinRule = useRoomJoinRule(room); const joinRule = useRoomJoinRule(room);
const canEditAvatar = canSendStateEvent(StateEvent.RoomAvatar, userPowerLevel); const canEditAvatar = permissions.stateEvent(StateEvent.RoomAvatar, mx.getSafeUserId());
const canEditName = canSendStateEvent(StateEvent.RoomName, userPowerLevel); const canEditName = permissions.stateEvent(StateEvent.RoomName, mx.getSafeUserId());
const canEditTopic = canSendStateEvent(StateEvent.RoomTopic, userPowerLevel); const canEditTopic = permissions.stateEvent(StateEvent.RoomTopic, mx.getSafeUserId());
const canEdit = canEditAvatar || canEditName || canEditTopic; const canEdit = canEditAvatar || canEditName || canEditTopic;
const avatarUrl = avatar const avatarUrl = avatar

View file

@ -8,23 +8,22 @@ import { SettingTile } from '../../../components/setting-tile';
import { useRoom } from '../../../hooks/useRoom'; import { useRoom } from '../../../hooks/useRoom';
import { useRoomDirectoryVisibility } from '../../../hooks/useRoomDirectoryVisibility'; import { useRoomDirectoryVisibility } from '../../../hooks/useRoomDirectoryVisibility';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { StateEvent } from '../../../../types/matrix/room'; import { StateEvent } from '../../../../types/matrix/room';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useStateEvent } from '../../../hooks/useStateEvent'; import { useStateEvent } from '../../../hooks/useStateEvent';
import { ExtendedJoinRules } from '../../../components/JoinRulesSwitcher'; import { ExtendedJoinRules } from '../../../components/JoinRulesSwitcher';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
type RoomPublishProps = { type RoomPublishProps = {
powerLevels: IPowerLevels; permissions: RoomPermissionsAPI;
}; };
export function RoomPublish({ powerLevels }: RoomPublishProps) { export function RoomPublish({ permissions }: RoomPublishProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
const canEditCanonical = powerLevelAPI.canSendStateEvent( const canEditCanonical = permissions.stateEvent(
powerLevels,
StateEvent.RoomCanonicalAlias, StateEvent.RoomCanonicalAlias,
userPowerLevel mx.getSafeUserId()
); );
const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules); const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules);
const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>(); const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>();

View file

@ -1,4 +1,4 @@
import React, { FormEventHandler, useCallback, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { import {
Button, Button,
color, color,
@ -14,54 +14,172 @@ import {
IconButton, IconButton,
Icon, Icon,
Icons, Icons,
Input,
} from 'folds'; } from 'folds';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { MatrixError } from 'matrix-js-sdk'; import { MatrixError, Method } from 'matrix-js-sdk';
import { RoomCreateEventContent, RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types'; import { RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types';
import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile'; import { SettingTile } from '../../../components/setting-tile';
import { useRoom } from '../../../hooks/useRoom'; import { useRoom } from '../../../hooks/useRoom';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels'; import { IRoomCreateContent, StateEvent } from '../../../../types/matrix/room';
import { StateEvent } from '../../../../types/matrix/room';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useStateEvent } from '../../../hooks/useStateEvent'; import { useStateEvent } from '../../../hooks/useStateEvent';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { useCapabilities } from '../../../hooks/useCapabilities'; import { useCapabilities } from '../../../hooks/useCapabilities';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
import {
AdditionalCreatorInput,
RoomVersionSelector,
useAdditionalCreators,
} from '../../../components/create-room';
import { useAlive } from '../../../hooks/useAlive';
import { creatorsSupported } from '../../../utils/matrix';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { BreakWord } from '../../../styles/Text.css';
function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
const mx = useMatrixClient();
const room = useRoom();
const alive = useAlive();
const creators = useRoomCreators(room);
const capabilities = useCapabilities();
const roomVersions = capabilities['m.room_versions'];
const [selectedRoomVersion, selectRoomVersion] = useState(roomVersions?.default ?? '1');
useEffect(() => {
// capabilities load async
selectRoomVersion(roomVersions?.default ?? '1');
}, [roomVersions?.default]);
const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
useAdditionalCreators(Array.from(creators));
const [upgradeState, upgrade] = useAsyncCallback(
useCallback(
async (version: string, newAdditionalCreators?: string[]) => {
await mx.http.authedRequest(Method.Post, `/rooms/${room.roomId}/upgrade`, undefined, {
new_version: version,
additional_creators: newAdditionalCreators,
});
},
[mx, room]
)
);
const upgrading = upgradeState.status === AsyncStatus.Loading;
const handleUpgradeRoom = () => {
const version = selectedRoomVersion;
upgrade(version, allowAdditionalCreators ? additionalCreators : undefined).then(() => {
if (alive()) {
requestClose();
}
});
};
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: requestClose,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}</Text>
</Box>
<IconButton size="300" onClick={requestClose} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Text priority="400" style={{ color: color.Critical.Main }}>
<b>This action is irreversible!</b>
</Text>
<Box direction="Column" gap="100">
<Text size="L400">Options</Text>
<RoomVersionSelector
versions={roomVersions?.available ? Object.keys(roomVersions.available) : ['1']}
value={selectedRoomVersion}
onChange={selectRoomVersion}
disabled={upgrading}
/>
{allowAdditionalCreators && (
<SequenceCard
style={{ padding: config.space.S300 }}
variant="SurfaceVariant"
direction="Column"
gap="500"
>
<AdditionalCreatorInput
additionalCreators={additionalCreators}
onSelect={addAdditionalCreator}
onRemove={removeAdditionalCreator}
disabled={upgrading}
/>
</SequenceCard>
)}
</Box>
{upgradeState.status === AsyncStatus.Error && (
<Text className={BreakWord} style={{ color: color.Critical.Main }} size="T200">
{(upgradeState.error as MatrixError).message}
</Text>
)}
<Button
onClick={handleUpgradeRoom}
variant="Secondary"
disabled={upgrading}
before={upgrading && <Spinner size="200" variant="Secondary" fill="Solid" />}
>
<Text size="B400">{room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}</Text>
</Button>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}
type RoomUpgradeProps = { type RoomUpgradeProps = {
powerLevels: IPowerLevels; permissions: RoomPermissionsAPI;
requestClose: () => void; requestClose: () => void;
}; };
export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) { export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const room = useRoom(); const room = useRoom();
const { navigateRoom, navigateSpace } = useRoomNavigate(); const { navigateRoom, navigateSpace } = useRoomNavigate();
const createContent = useStateEvent( const createContent = useStateEvent(
room, room,
StateEvent.RoomCreate StateEvent.RoomCreate
)?.getContent<RoomCreateEventContent>(); )?.getContent<IRoomCreateContent>();
const roomVersion = createContent?.room_version ?? 1; const roomVersion = createContent?.room_version ?? '1';
const predecessorRoomId = createContent?.predecessor?.room_id; const predecessorRoomId = createContent?.predecessor?.room_id;
const capabilities = useCapabilities();
const defaultRoomVersion = capabilities['m.room_versions']?.default;
const tombstoneContent = useStateEvent( const tombstoneContent = useStateEvent(
room, room,
StateEvent.RoomTombstone StateEvent.RoomTombstone
)?.getContent<RoomTombstoneEventContent>(); )?.getContent<RoomTombstoneEventContent>();
const replacementRoom = tombstoneContent?.replacement_room; const replacementRoom = tombstoneContent?.replacement_room;
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); const canUpgrade = permissions.stateEvent(StateEvent.RoomTombstone, mx.getSafeUserId());
const canUpgrade = powerLevelAPI.canSendStateEvent(
powerLevels,
StateEvent.RoomTombstone,
userPowerLevel
);
const handleOpenRoom = () => { const handleOpenRoom = () => {
if (replacementRoom) { if (replacementRoom) {
@ -85,31 +203,8 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
} }
}; };
const [upgradeState, upgrade] = useAsyncCallback(
useCallback(
async (version: string) => {
await mx.upgradeRoom(room.roomId, version);
},
[mx, room]
)
);
const upgrading = upgradeState.status === AsyncStatus.Loading;
const [prompt, setPrompt] = useState(false); const [prompt, setPrompt] = useState(false);
const handleSubmitUpgrade: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
const target = evt.target as HTMLFormElement | undefined;
const versionInput = target?.versionInput as HTMLInputElement | undefined;
const version = versionInput?.value.trim();
if (!version) return;
upgrade(version);
setPrompt(false);
};
return ( return (
<SequenceCard <SequenceCard
className={SequenceCardStyle} className={SequenceCardStyle}
@ -123,7 +218,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
replacementRoom replacementRoom
? tombstoneContent.body || ? tombstoneContent.body ||
`This ${room.isSpaceRoom() ? 'space' : 'room'} has been replaced!` `This ${room.isSpaceRoom() ? 'space' : 'room'} has been replaced!`
: `Current room version: ${roomVersion}.` : `Current version: ${roomVersion}.`
} }
after={ after={
<Box alignItems="Center" gap="200"> <Box alignItems="Center" gap="200">
@ -155,8 +250,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
variant="Secondary" variant="Secondary"
fill="Solid" fill="Solid"
radii="300" radii="300"
disabled={upgrading || !canUpgrade} disabled={!canUpgrade}
before={upgrading && <Spinner size="100" variant="Secondary" fill="Solid" />}
onClick={() => setPrompt(true)} onClick={() => setPrompt(true)}
> >
<Text size="B300">Upgrade</Text> <Text size="B300">Upgrade</Text>
@ -165,63 +259,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
</Box> </Box>
} }
> >
{upgradeState.status === AsyncStatus.Error && ( {prompt && <RoomUpgradeDialog requestClose={() => setPrompt(false)} />}
<Text style={{ color: color.Critical.Main }} size="T200">
{(upgradeState.error as MatrixError).message}
</Text>
)}
{prompt && (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setPrompt(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface" as="form" onSubmit={handleSubmitUpgrade}>
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}</Text>
</Box>
<IconButton size="300" onClick={() => setPrompt(false)} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Text priority="400" style={{ color: color.Critical.Main }}>
<b>This action is irreversible!</b>
</Text>
<Box direction="Column" gap="100">
<Text size="L400">Version</Text>
<Input
defaultValue={defaultRoomVersion}
name="versionInput"
variant="Background"
required
/>
</Box>
<Button type="submit" variant="Secondary">
<Text size="B400">
{room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}
</Text>
</Button>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
)}
</SettingTile> </SettingTile>
</SequenceCard> </SequenceCard>
); );

View file

@ -27,11 +27,7 @@ import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom'; import { useRoom } from '../../../hooks/useRoom';
import { useRoomMembers } from '../../../hooks/useRoomMembers'; import { useRoomMembers } from '../../../hooks/useRoomMembers';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels'; import { usePowerLevels } from '../../../hooks/usePowerLevels';
import {
useFlattenPowerLevelTagMembers,
usePowerLevelTags,
} from '../../../hooks/usePowerLevelTags';
import { VirtualTile } from '../../../components/virtualizer'; import { VirtualTile } from '../../../components/virtualizer';
import { MemberTile } from '../../../components/member-tile'; import { MemberTile } from '../../../components/member-tile';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
@ -45,7 +41,7 @@ import {
} from '../../../hooks/useAsyncSearch'; } from '../../../hooks/useAsyncSearch';
import { getMemberSearchStr } from '../../../utils/room'; import { getMemberSearchStr } from '../../../utils/room';
import { useMembershipFilter, useMembershipFilterMenu } from '../../../hooks/useMemberFilter'; import { useMembershipFilter, useMembershipFilterMenu } from '../../../hooks/useMemberFilter';
import { useMemberSort, useMemberSortMenu } from '../../../hooks/useMemberSort'; import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../../hooks/useMemberSort';
import { settingsAtom } from '../../../state/settings'; import { settingsAtom } from '../../../state/settings';
import { useSetting } from '../../../state/hooks/settings'; import { useSetting } from '../../../state/hooks/settings';
import { UseStateProvider } from '../../../components/UseStateProvider'; import { UseStateProvider } from '../../../components/UseStateProvider';
@ -57,6 +53,8 @@ import {
useUserRoomProfileState, useUserRoomProfileState,
} from '../../../state/hooks/userRoomProfile'; } from '../../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../../hooks/useSpace'; import { useSpaceOptionally } from '../../../hooks/useSpace';
import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../../hooks/useMemberPowerTag';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
const SEARCH_OPTIONS: UseAsyncSearchOptions = { const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000, limit: 1000,
@ -86,13 +84,14 @@ export function Members({ requestClose }: MembersProps) {
const space = useSpaceOptionally(); const space = useSpaceOptionally();
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel } = usePowerLevelsAPI(powerLevels); const creators = useRoomCreators(room);
const [, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
const [membershipFilterIndex, setMembershipFilterIndex] = useState(0); const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex'); const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
const membershipFilter = useMembershipFilter(membershipFilterIndex, useMembershipFilterMenu()); const membershipFilter = useMembershipFilter(membershipFilterIndex, useMembershipFilterMenu());
const memberSort = useMemberSort(sortFilterIndex, useMemberSortMenu()); const memberSort = useMemberSort(sortFilterIndex, useMemberSortMenu());
const memberPowerSort = useMemberPowerSort(creators);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
@ -103,8 +102,8 @@ export function Members({ requestClose }: MembersProps) {
Array.from(members) Array.from(members)
.filter(membershipFilter.filterFn) .filter(membershipFilter.filterFn)
.sort(memberSort.sortFn) .sort(memberSort.sortFn)
.sort((a, b) => b.powerLevel - a.powerLevel), .sort(memberPowerSort),
[members, membershipFilter, memberSort] [members, membershipFilter, memberSort, memberPowerSort]
); );
const [result, search, resetSearch] = useAsyncSearch( const [result, search, resetSearch] = useAsyncSearch(
@ -114,11 +113,7 @@ export function Members({ requestClose }: MembersProps) {
); );
if (!result && searchInputRef.current?.value) search(searchInputRef.current.value); if (!result && searchInputRef.current?.value) search(searchInputRef.current.value);
const flattenTagMembers = useFlattenPowerLevelTagMembers( const flattenTagMembers = useFlattenPowerTagMembers(result?.items ?? sortedMembers, getPowerTag);
result?.items ?? sortedMembers,
getPowerLevel,
getPowerLevelTag
);
const virtualizer = useVirtualizer({ const virtualizer = useVirtualizer({
count: flattenTagMembers.length, count: flattenTagMembers.length,

Some files were not shown because too many files have changed in this diff Show more