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:
parent
4d1ae4eafd
commit
f82cfead46
56 changed files with 1715 additions and 781 deletions
306
src/app/components/create-room/AdditionalCreatorInput.tsx
Normal file
306
src/app/components/create-room/AdditionalCreatorInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ export function RoomVersionSelector({
|
|||
gap="500"
|
||||
>
|
||||
<SettingTile
|
||||
title="Room Version"
|
||||
title="Version"
|
||||
after={
|
||||
<PopOut
|
||||
anchor={menuCords}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ export * from './CreateRoomKindSelector';
|
|||
export * from './CreateRoomAliasInput';
|
||||
export * from './RoomVersionSelector';
|
||||
export * from './utils';
|
||||
export * from './AdditionalCreatorInput';
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import { getMxIdServer } from '../../utils/matrix';
|
|||
|
||||
export const createRoomCreationContent = (
|
||||
type: RoomType | undefined,
|
||||
allowFederation: boolean
|
||||
allowFederation: boolean,
|
||||
additionalCreators: string[] | undefined
|
||||
): object => {
|
||||
const content: Record<string, any> = {};
|
||||
if (typeof type === 'string') {
|
||||
|
|
@ -23,6 +24,9 @@ export const createRoomCreationContent = (
|
|||
if (allowFederation === false) {
|
||||
content['m.federate'] = false;
|
||||
}
|
||||
if (Array.isArray(additionalCreators)) {
|
||||
content.additional_creators = additionalCreators;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
|
@ -89,6 +93,7 @@ export type CreateRoomData = {
|
|||
encryption?: boolean;
|
||||
knock: boolean;
|
||||
allowFederation: boolean;
|
||||
additionalCreators?: string[];
|
||||
};
|
||||
export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise<string> => {
|
||||
const initialState: ICreateRoomStateEvent[] = [];
|
||||
|
|
@ -108,7 +113,11 @@ export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promis
|
|||
name: data.name,
|
||||
topic: data.topic,
|
||||
room_alias_name: data.aliasLocalPart,
|
||||
creation_content: createRoomCreationContent(data.type, data.allowFederation),
|
||||
creation_content: createRoomCreationContent(
|
||||
data.type,
|
||||
data.allowFederation,
|
||||
data.additionalCreators
|
||||
),
|
||||
initial_state: initialState,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import { usePowerLevels } from '../../hooks/usePowerLevels';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { ImagePackContent } from './ImagePackContent';
|
||||
import { ImagePack, PackContent } from '../../plugins/custom-emoji';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { useRoomImagePack } from '../../hooks/useImagePacks';
|
||||
import { randomStr } from '../../utils/common';
|
||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
|
||||
type RoomImagePackProps = {
|
||||
room: Room;
|
||||
|
|
@ -17,9 +19,10 @@ export function RoomImagePack({ room, stateKey }: RoomImagePackProps) {
|
|||
const mx = useMatrixClient();
|
||||
const userId = mx.getUserId()!;
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const creators = useRoomCreators(room);
|
||||
|
||||
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
|
||||
const canEditImagePack = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(userId));
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
const canEditImagePack = permissions.stateEvent(StateEvent.PoniesRoomEmotes, userId);
|
||||
|
||||
const fallbackPack = useMemo(() => {
|
||||
const fakePackId = randomStr(4);
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import * as css from './Reply.css';
|
|||
import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content';
|
||||
import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
|
||||
import { useRoomEvent } from '../../hooks/useRoomEvent';
|
||||
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { GetMemberPowerTag } from '../../hooks/useMemberPowerTag';
|
||||
|
||||
type ReplyLayoutProps = {
|
||||
userColor?: string;
|
||||
|
|
@ -57,8 +57,7 @@ type ReplyProps = {
|
|||
replyEventId: string;
|
||||
threadRootId?: string | undefined;
|
||||
onClick?: MouseEventHandler | undefined;
|
||||
getPowerLevel?: (userId: string) => number;
|
||||
getPowerLevelTag?: GetPowerLevelTag;
|
||||
getMemberPowerTag?: GetMemberPowerTag;
|
||||
accessibleTagColors?: Map<string, string>;
|
||||
legacyUsernameColor?: boolean;
|
||||
};
|
||||
|
|
@ -71,8 +70,7 @@ export const Reply = as<'div', ReplyProps>(
|
|||
replyEventId,
|
||||
threadRootId,
|
||||
onClick,
|
||||
getPowerLevel,
|
||||
getPowerLevelTag,
|
||||
getMemberPowerTag,
|
||||
accessibleTagColors,
|
||||
legacyUsernameColor,
|
||||
...props
|
||||
|
|
@ -88,8 +86,7 @@ export const Reply = as<'div', ReplyProps>(
|
|||
|
||||
const { body } = replyEvent?.getContent() ?? {};
|
||||
const sender = replyEvent?.getSender();
|
||||
const senderPL = sender && getPowerLevel?.(sender);
|
||||
const powerTag = typeof senderPL === 'number' ? getPowerLevelTag?.(senderPL) : undefined;
|
||||
const powerTag = sender ? getMemberPowerTag?.(sender) : undefined;
|
||||
const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined;
|
||||
|
||||
const usernameColor = legacyUsernameColor ? colorMXID(sender ?? replyEventId) : tagColor;
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ export const RoomIntro = as<'div', RoomIntroProps>(({ room, ...props }, ref) =>
|
|||
{typeof prevRoomId === 'string' &&
|
||||
(mx.getRoom(prevRoomId)?.getMyMembership() === Membership.Join ? (
|
||||
<Button
|
||||
onClick={() => navigateRoom(prevRoomId)}
|
||||
onClick={() => navigateRoom(prevRoomId, createContent?.predecessor?.event_id)}
|
||||
variant="Success"
|
||||
size="300"
|
||||
fill="Soft"
|
||||
|
|
|
|||
101
src/app/components/user-profile/CreatorChip.tsx
Normal file
101
src/app/components/user-profile/CreatorChip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -26,8 +26,8 @@ import { isKeyHotkey } from 'is-hotkey';
|
|||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { PowerColorBadge, PowerIcon } from '../power';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import { getPowers, getTagIconSrc, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||
import { useGetMemberPowerLevel, usePowerLevels } from '../../hooks/usePowerLevels';
|
||||
import { getPowers, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
|
||||
|
|
@ -39,6 +39,10 @@ import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
|||
import { SpaceSettingsPage } from '../../state/spaceSettings';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
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 = {
|
||||
power: number;
|
||||
|
|
@ -149,16 +153,22 @@ export function PowerChip({ userId }: { userId: string }) {
|
|||
const openSpaceSettings = useOpenSpaceSettings();
|
||||
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
|
||||
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 creators = useRoomCreators(room);
|
||||
|
||||
const tag = getPowerLevelTag(userPower);
|
||||
const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon);
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
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>();
|
||||
|
||||
|
|
@ -184,13 +194,13 @@ export function PowerChip({ userId }: { userId: string }) {
|
|||
const handlePowerSelect = (power: number): void => {
|
||||
close();
|
||||
if (!canChangePowers) return;
|
||||
if (power === userPower) return;
|
||||
if (power === getMemberPowerLevel(userId)) return;
|
||||
|
||||
if (userId === mx.getSafeUserId()) {
|
||||
setSelfDemote(power);
|
||||
return;
|
||||
}
|
||||
if (power === myPower) {
|
||||
if (!creators.has(myUserId) && power === getMemberPowerLevel(myUserId)) {
|
||||
setSharedPower(power);
|
||||
return;
|
||||
}
|
||||
|
|
@ -242,19 +252,22 @@ export function PowerChip({ userId }: { userId: string }) {
|
|||
{getPowers(powerLevelTags).map((power) => {
|
||||
const powerTag = powerLevelTags[power];
|
||||
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 (
|
||||
<MenuItem
|
||||
key={power}
|
||||
variant={userPower === power ? 'Primary' : 'Surface'}
|
||||
variant={selected ? 'Primary' : 'Surface'}
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-disabled={changing || !canChangePowers || !canAssignPower}
|
||||
aria-pressed={userPower === power}
|
||||
aria-pressed={selected}
|
||||
before={<PowerColorBadge color={powerTag.color} />}
|
||||
after={
|
||||
powerTagIconSrc ? (
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import { getDMRoomFor, getMxIdServer, mxcUrlToHttp } from '../../utils/matrix';
|
|||
import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import { usePowerLevels } from '../../hooks/usePowerLevels';
|
||||
import { useRoom } from '../../hooks/useRoom';
|
||||
import { useUserPresence } from '../../hooks/useUserPresence';
|
||||
import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips';
|
||||
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 { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { useAlive } from '../../hooks/useAlive';
|
||||
|
|
@ -20,6 +20,10 @@ import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './
|
|||
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
|
||||
import { useMembership } from '../../hooks/useMembership';
|
||||
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 = {
|
||||
userId: string;
|
||||
|
|
@ -34,13 +38,19 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||
const ignored = ignoredUsers.includes(userId);
|
||||
|
||||
const room = useRoom();
|
||||
const powerlevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerlevels);
|
||||
const myPowerLevel = getPowerLevel(mx.getSafeUserId());
|
||||
const userPowerLevel = getPowerLevel(userId);
|
||||
const canKick = canDoAction('kick', myPowerLevel) && myPowerLevel > userPowerLevel;
|
||||
const canBan = canDoAction('ban', myPowerLevel) && myPowerLevel > userPowerLevel;
|
||||
const canInvite = canDoAction('invite', myPowerLevel);
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const creators = useRoomCreators(room);
|
||||
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
const { hasMorePower } = useMemberPowerCompare(creators, powerLevels);
|
||||
|
||||
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 membership = useMembership(room, userId);
|
||||
|
|
@ -113,7 +123,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||
<Box alignItems="Center" gap="200" wrap="Wrap">
|
||||
{server && <ServerChip server={server} />}
|
||||
<ShareChip userId={userId} />
|
||||
<PowerChip userId={userId} />
|
||||
{creator ? <CreatorChip /> : <PowerChip userId={userId} />}
|
||||
<MutualRoomsChip userId={userId} />
|
||||
<OptionsChip userId={userId} />
|
||||
</Box>
|
||||
|
|
@ -123,7 +133,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||
<UserBanAlert
|
||||
userId={userId}
|
||||
reason={member.events.member?.getContent().reason}
|
||||
canUnban={canBan}
|
||||
canUnban={canUnban}
|
||||
bannedBy={member.events.member?.getSender()}
|
||||
ts={member.events.member?.getTs()}
|
||||
/>
|
||||
|
|
@ -142,7 +152,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||
<UserInviteAlert
|
||||
userId={userId}
|
||||
reason={member.events.member?.getContent().reason}
|
||||
canKick={canKick}
|
||||
canKick={canKickUser}
|
||||
invitedBy={member.events.member?.getSender()}
|
||||
ts={member.events.member?.getTs()}
|
||||
/>
|
||||
|
|
@ -150,8 +160,8 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||
<UserModeration
|
||||
userId={userId}
|
||||
canInvite={canInvite && membership === Membership.Leave}
|
||||
canKick={canKick && membership === Membership.Join}
|
||||
canBan={canBan && membership !== Membership.Ban}
|
||||
canKick={canKickUser && membership === Membership.Join}
|
||||
canBan={canBanUser && membership !== Membership.Ban}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
|||
import { syntaxErrorPosition } from '../../../utils/dom';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { useTextAreaCodeEditor } from '../../../hooks/useTextAreaCodeEditor';
|
||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
||||
|
||||
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 [editContent, setEditContent] = useState<object>();
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
|
||||
const canEdit = canSendStateEvent(type, getPowerLevel(mx.getSafeUserId()));
|
||||
const creators = useRoomCreators(room);
|
||||
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
const canEdit = permissions.stateEvent(type, mx.getSafeUserId());
|
||||
|
||||
const eventJSONStr = useMemo(() => {
|
||||
if (!stateEvent) return '';
|
||||
|
|
|
|||
|
|
@ -33,11 +33,13 @@ import { SequenceCardStyle } from '../styles.css';
|
|||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { suffixRename } from '../../../utils/common';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
||||
|
||||
type CreatePackTileProps = {
|
||||
packs: ImagePack[];
|
||||
|
|
@ -146,8 +148,10 @@ export function RoomPacks({ onViewPack }: RoomPacksProps) {
|
|||
const alive = useAlive();
|
||||
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { canSendStateEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels);
|
||||
const canEdit = canSendStateEvent(StateEvent.PoniesRoomEmotes, getPowerLevel(mx.getSafeUserId()));
|
||||
const creators = useRoomCreators(room);
|
||||
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
const canEdit = permissions.stateEvent(StateEvent.PoniesRoomEmotes, mx.getSafeUserId());
|
||||
|
||||
const unfilteredPacks = useRoomImagePacks(room);
|
||||
const packs = useMemo(() => unfilteredPacks.filter((pack) => !pack.deleted), [unfilteredPacks]);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
toRem,
|
||||
} from 'folds';
|
||||
import { MatrixError } from 'matrix-js-sdk';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
|
|
@ -33,19 +32,19 @@ import { getIdServer } from '../../../../util/matrixUtil';
|
|||
import { replaceSpaceWithDash } from '../../../utils/common';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
|
||||
|
||||
type RoomPublishedAddressesProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissions: RoomPermissionsAPI;
|
||||
};
|
||||
|
||||
export function RoomPublishedAddresses({ powerLevels }: RoomPublishedAddressesProps) {
|
||||
export function RoomPublishedAddresses({ permissions }: RoomPublishedAddressesProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
|
||||
const canEditCanonical = powerLevelAPI.canSendStateEvent(
|
||||
powerLevels,
|
||||
|
||||
const canEditCanonical = permissions.stateEvent(
|
||||
StateEvent.RoomCanonicalAlias,
|
||||
userPowerLevel
|
||||
mx.getSafeUserId()
|
||||
);
|
||||
|
||||
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 room = useRoom();
|
||||
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
|
||||
const canEditCanonical = powerLevelAPI.canSendStateEvent(
|
||||
powerLevels,
|
||||
|
||||
const canEditCanonical = permissions.stateEvent(
|
||||
StateEvent.RoomCanonicalAlias,
|
||||
userPowerLevel
|
||||
mx.getSafeUserId()
|
||||
);
|
||||
|
||||
const [expand, setExpand] = useState(false);
|
||||
|
|
|
|||
|
|
@ -21,28 +21,24 @@ import FocusTrap from 'focus-trap-react';
|
|||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { useStateEvent } from '../../../hooks/useStateEvent';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
|
||||
|
||||
const ROOM_ENC_ALGO = 'm.megolm.v1.aes-sha2';
|
||||
|
||||
type RoomEncryptionProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissions: RoomPermissionsAPI;
|
||||
};
|
||||
export function RoomEncryption({ powerLevels }: RoomEncryptionProps) {
|
||||
export function RoomEncryption({ permissions }: RoomEncryptionProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
|
||||
const canEnable = powerLevelAPI.canSendStateEvent(
|
||||
powerLevels,
|
||||
StateEvent.RoomEncryption,
|
||||
userPowerLevel
|
||||
);
|
||||
|
||||
const canEnable = permissions.stateEvent(StateEvent.RoomEncryption, mx.getSafeUserId());
|
||||
const content = useStateEvent(room, StateEvent.RoomEncryption)?.getContent<{
|
||||
algorithm: string;
|
||||
}>();
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ import FocusTrap from 'focus-trap-react';
|
|||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useStateEvent } from '../../../hooks/useStateEvent';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
|
||||
|
||||
const useVisibilityStr = () =>
|
||||
useMemo(
|
||||
|
|
@ -49,17 +49,13 @@ const useVisibilityMenu = () =>
|
|||
);
|
||||
|
||||
type RoomHistoryVisibilityProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissions: RoomPermissionsAPI;
|
||||
};
|
||||
export function RoomHistoryVisibility({ powerLevels }: RoomHistoryVisibilityProps) {
|
||||
export function RoomHistoryVisibility({ permissions }: RoomHistoryVisibilityProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
|
||||
const canEdit = powerLevelAPI.canSendStateEvent(
|
||||
powerLevels,
|
||||
StateEvent.RoomHistoryVisibility,
|
||||
userPowerLevel
|
||||
);
|
||||
|
||||
const canEdit = permissions.stateEvent(StateEvent.RoomHistoryVisibility, mx.getSafeUserId());
|
||||
|
||||
const visibilityEvent = useStateEvent(room, StateEvent.RoomHistoryVisibility);
|
||||
const historyVisibility: HistoryVisibility =
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { color, Text } from 'folds';
|
|||
import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk';
|
||||
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import {
|
||||
ExtendedJoinRules,
|
||||
JoinRulesSwitcher,
|
||||
|
|
@ -32,6 +31,7 @@ import {
|
|||
knockSupported,
|
||||
restrictedSupported,
|
||||
} from '../../../utils/matrix';
|
||||
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
|
||||
|
||||
type RestrictedRoomAllowContent = {
|
||||
room_id: string;
|
||||
|
|
@ -39,9 +39,9 @@ type RestrictedRoomAllowContent = {
|
|||
};
|
||||
|
||||
type RoomJoinRulesProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissions: RoomPermissionsAPI;
|
||||
};
|
||||
export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
||||
export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const allowKnockRestricted = knockRestrictedSupported(room.getVersion());
|
||||
|
|
@ -53,12 +53,7 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
|
|||
const subspacesScope = useRecursiveChildSpaceScopeFactory(mx, roomIdToParents);
|
||||
const subspaces = useSpaceChildren(allRoomsAtom, space?.roomId ?? '', subspacesScope);
|
||||
|
||||
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
|
||||
const canEdit = powerLevelAPI.canSendStateEvent(
|
||||
powerLevels,
|
||||
StateEvent.RoomHistoryVisibility,
|
||||
userPowerLevel
|
||||
);
|
||||
const canEdit = permissions.stateEvent(StateEvent.RoomHistoryVisibility, mx.getSafeUserId());
|
||||
|
||||
const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules);
|
||||
const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>();
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import { RoomAvatar, RoomIcon } from '../../../components/room-avatar';
|
|||
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
import { IPowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { CompactUploadCardRenderer } from '../../../components/upload-card';
|
||||
import { useObjectURL } from '../../../hooks/useObjectURL';
|
||||
|
|
@ -40,6 +39,7 @@ import { createUploadAtom, UploadSuccess } from '../../../state/upload';
|
|||
import { useFilePicker } from '../../../hooks/useFilePicker';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
|
||||
|
||||
type RoomProfileEditProps = {
|
||||
canEditAvatar: boolean;
|
||||
|
|
@ -261,24 +261,22 @@ export function RoomProfileEdit({
|
|||
}
|
||||
|
||||
type RoomProfileProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissions: RoomPermissionsAPI;
|
||||
};
|
||||
export function RoomProfile({ powerLevels }: RoomProfileProps) {
|
||||
export function RoomProfile({ permissions }: RoomProfileProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const room = useRoom();
|
||||
const directs = useAtomValue(mDirectAtom);
|
||||
const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
|
||||
const userPowerLevel = getPowerLevel(mx.getSafeUserId());
|
||||
|
||||
const avatar = useRoomAvatar(room, directs.has(room.roomId));
|
||||
const name = useRoomName(room);
|
||||
const topic = useRoomTopic(room);
|
||||
const joinRule = useRoomJoinRule(room);
|
||||
|
||||
const canEditAvatar = canSendStateEvent(StateEvent.RoomAvatar, userPowerLevel);
|
||||
const canEditName = canSendStateEvent(StateEvent.RoomName, userPowerLevel);
|
||||
const canEditTopic = canSendStateEvent(StateEvent.RoomTopic, userPowerLevel);
|
||||
const canEditAvatar = permissions.stateEvent(StateEvent.RoomAvatar, mx.getSafeUserId());
|
||||
const canEditName = permissions.stateEvent(StateEvent.RoomName, mx.getSafeUserId());
|
||||
const canEditTopic = permissions.stateEvent(StateEvent.RoomTopic, mx.getSafeUserId());
|
||||
const canEdit = canEditAvatar || canEditName || canEditTopic;
|
||||
|
||||
const avatarUrl = avatar
|
||||
|
|
|
|||
|
|
@ -8,23 +8,22 @@ import { SettingTile } from '../../../components/setting-tile';
|
|||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { useRoomDirectoryVisibility } from '../../../hooks/useRoomDirectoryVisibility';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useStateEvent } from '../../../hooks/useStateEvent';
|
||||
import { ExtendedJoinRules } from '../../../components/JoinRulesSwitcher';
|
||||
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
|
||||
|
||||
type RoomPublishProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissions: RoomPermissionsAPI;
|
||||
};
|
||||
export function RoomPublish({ powerLevels }: RoomPublishProps) {
|
||||
export function RoomPublish({ permissions }: RoomPublishProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
|
||||
const canEditCanonical = powerLevelAPI.canSendStateEvent(
|
||||
powerLevels,
|
||||
|
||||
const canEditCanonical = permissions.stateEvent(
|
||||
StateEvent.RoomCanonicalAlias,
|
||||
userPowerLevel
|
||||
mx.getSafeUserId()
|
||||
);
|
||||
const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules);
|
||||
const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FormEventHandler, useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
color,
|
||||
|
|
@ -14,54 +14,172 @@ import {
|
|||
IconButton,
|
||||
Icon,
|
||||
Icons,
|
||||
Input,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { MatrixError } from 'matrix-js-sdk';
|
||||
import { RoomCreateEventContent, RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { MatrixError, Method } from 'matrix-js-sdk';
|
||||
import { RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
|
||||
import { StateEvent } from '../../../../types/matrix/room';
|
||||
import { IRoomCreateContent, StateEvent } from '../../../../types/matrix/room';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useStateEvent } from '../../../hooks/useStateEvent';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { useCapabilities } from '../../../hooks/useCapabilities';
|
||||
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 = {
|
||||
powerLevels: IPowerLevels;
|
||||
permissions: RoomPermissionsAPI;
|
||||
requestClose: () => void;
|
||||
};
|
||||
export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
||||
export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
|
||||
const mx = useMatrixClient();
|
||||
const room = useRoom();
|
||||
const { navigateRoom, navigateSpace } = useRoomNavigate();
|
||||
const createContent = useStateEvent(
|
||||
room,
|
||||
StateEvent.RoomCreate
|
||||
)?.getContent<RoomCreateEventContent>();
|
||||
const roomVersion = createContent?.room_version ?? 1;
|
||||
)?.getContent<IRoomCreateContent>();
|
||||
const roomVersion = createContent?.room_version ?? '1';
|
||||
const predecessorRoomId = createContent?.predecessor?.room_id;
|
||||
|
||||
const capabilities = useCapabilities();
|
||||
const defaultRoomVersion = capabilities['m.room_versions']?.default;
|
||||
|
||||
const tombstoneContent = useStateEvent(
|
||||
room,
|
||||
StateEvent.RoomTombstone
|
||||
)?.getContent<RoomTombstoneEventContent>();
|
||||
const replacementRoom = tombstoneContent?.replacement_room;
|
||||
|
||||
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
|
||||
const canUpgrade = powerLevelAPI.canSendStateEvent(
|
||||
powerLevels,
|
||||
StateEvent.RoomTombstone,
|
||||
userPowerLevel
|
||||
);
|
||||
const canUpgrade = permissions.stateEvent(StateEvent.RoomTombstone, mx.getSafeUserId());
|
||||
|
||||
const handleOpenRoom = () => {
|
||||
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 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 (
|
||||
<SequenceCard
|
||||
className={SequenceCardStyle}
|
||||
|
|
@ -123,7 +218,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
replacementRoom
|
||||
? tombstoneContent.body ||
|
||||
`This ${room.isSpaceRoom() ? 'space' : 'room'} has been replaced!`
|
||||
: `Current room version: ${roomVersion}.`
|
||||
: `Current version: ${roomVersion}.`
|
||||
}
|
||||
after={
|
||||
<Box alignItems="Center" gap="200">
|
||||
|
|
@ -155,8 +250,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
variant="Secondary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
disabled={upgrading || !canUpgrade}
|
||||
before={upgrading && <Spinner size="100" variant="Secondary" fill="Solid" />}
|
||||
disabled={!canUpgrade}
|
||||
onClick={() => setPrompt(true)}
|
||||
>
|
||||
<Text size="B300">Upgrade</Text>
|
||||
|
|
@ -165,63 +259,7 @@ export function RoomUpgrade({ powerLevels, requestClose }: RoomUpgradeProps) {
|
|||
</Box>
|
||||
}
|
||||
>
|
||||
{upgradeState.status === AsyncStatus.Error && (
|
||||
<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>
|
||||
)}
|
||||
{prompt && <RoomUpgradeDialog requestClose={() => setPrompt(false)} />}
|
||||
</SettingTile>
|
||||
</SequenceCard>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,11 +27,7 @@ import { Page, PageContent, PageHeader } from '../../../components/page';
|
|||
import { useRoom } from '../../../hooks/useRoom';
|
||||
import { useRoomMembers } from '../../../hooks/useRoomMembers';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
|
||||
import {
|
||||
useFlattenPowerLevelTagMembers,
|
||||
usePowerLevelTags,
|
||||
} from '../../../hooks/usePowerLevelTags';
|
||||
import { usePowerLevels } from '../../../hooks/usePowerLevels';
|
||||
import { VirtualTile } from '../../../components/virtualizer';
|
||||
import { MemberTile } from '../../../components/member-tile';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
|
|
@ -45,7 +41,7 @@ import {
|
|||
} from '../../../hooks/useAsyncSearch';
|
||||
import { getMemberSearchStr } from '../../../utils/room';
|
||||
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 { useSetting } from '../../../state/hooks/settings';
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
|
|
@ -57,6 +53,8 @@ import {
|
|||
useUserRoomProfileState,
|
||||
} from '../../../state/hooks/userRoomProfile';
|
||||
import { useSpaceOptionally } from '../../../hooks/useSpace';
|
||||
import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../../hooks/useMemberPowerTag';
|
||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||
|
||||
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
|
||||
limit: 1000,
|
||||
|
|
@ -86,13 +84,14 @@ export function Members({ requestClose }: MembersProps) {
|
|||
const space = useSpaceOptionally();
|
||||
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
|
||||
const [, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
|
||||
const creators = useRoomCreators(room);
|
||||
const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
|
||||
|
||||
const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
|
||||
const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
|
||||
const membershipFilter = useMembershipFilter(membershipFilterIndex, useMembershipFilterMenu());
|
||||
const memberSort = useMemberSort(sortFilterIndex, useMemberSortMenu());
|
||||
const memberPowerSort = useMemberPowerSort(creators);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -103,8 +102,8 @@ export function Members({ requestClose }: MembersProps) {
|
|||
Array.from(members)
|
||||
.filter(membershipFilter.filterFn)
|
||||
.sort(memberSort.sortFn)
|
||||
.sort((a, b) => b.powerLevel - a.powerLevel),
|
||||
[members, membershipFilter, memberSort]
|
||||
.sort(memberPowerSort),
|
||||
[members, membershipFilter, memberSort, memberPowerSort]
|
||||
);
|
||||
|
||||
const [result, search, resetSearch] = useAsyncSearch(
|
||||
|
|
@ -114,11 +113,7 @@ export function Members({ requestClose }: MembersProps) {
|
|||
);
|
||||
if (!result && searchInputRef.current?.value) search(searchInputRef.current.value);
|
||||
|
||||
const flattenTagMembers = useFlattenPowerLevelTagMembers(
|
||||
result?.items ?? sortedMembers,
|
||||
getPowerLevel,
|
||||
getPowerLevelTag
|
||||
);
|
||||
const flattenTagMembers = useFlattenPowerTagMembers(result?.items ?? sortedMembers, getPowerTag);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: flattenTagMembers.length,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue