--- /dev/null
+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>
+ );
+}
gap="500"
>
<SettingTile
- title="Room Version"
+ title="Version"
after={
<PopOut
anchor={menuCords}
export * from './CreateRoomAliasInput';
export * from './RoomVersionSelector';
export * from './utils';
+export * from './AdditionalCreatorInput';
export const createRoomCreationContent = (
type: RoomType | undefined,
- allowFederation: boolean
+ allowFederation: boolean,
+ additionalCreators: string[] | undefined
): object => {
const content: Record<string, any> = {};
if (typeof type === 'string') {
if (allowFederation === false) {
content['m.federate'] = false;
}
+ if (Array.isArray(additionalCreators)) {
+ content.additional_creators = additionalCreators;
+ }
return content;
};
encryption?: boolean;
knock: boolean;
allowFederation: boolean;
+ additionalCreators?: string[];
};
export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise<string> => {
const initialState: ICreateRoomStateEvent[] = [];
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,
};
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;
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);
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;
replyEventId: string;
threadRootId?: string | undefined;
onClick?: MouseEventHandler | undefined;
- getPowerLevel?: (userId: string) => number;
- getPowerLevelTag?: GetPowerLevelTag;
+ getMemberPowerTag?: GetMemberPowerTag;
accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean;
};
replyEventId,
threadRootId,
onClick,
- getPowerLevel,
- getPowerLevelTag,
+ getMemberPowerTag,
accessibleTagColors,
legacyUsernameColor,
...props
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;
{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"
--- /dev/null
+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>
+ );
+}
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';
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;
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 creators = useRoomCreators(room);
+
+ 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 =
- canSendStateEvent(StateEvent.RoomPowerLevels, myPower) &&
- (mx.getSafeUserId() === userId ? true : myPower > userPower);
+ permissions.stateEvent(StateEvent.RoomPowerLevels, myUserId) &&
+ (myUserId === userId ? true : hasMorePower(myUserId, userId));
- const tag = getPowerLevelTag(userPower);
- const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon);
+ const tag = getMemberPowerTag(userId);
+ const tagIconSrc = tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon);
const [cords, setCords] = useState<RectCords>();
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;
}
{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 ? (
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';
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;
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);
<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>
<UserBanAlert
userId={userId}
reason={member.events.member?.getContent().reason}
- canUnban={canBan}
+ canUnban={canUnban}
bannedBy={member.events.member?.getSender()}
ts={member.events.member?.getTs()}
/>
<UserInviteAlert
userId={userId}
reason={member.events.member?.getContent().reason}
- canKick={canKick}
+ canKick={canKickUser}
invitedBy={member.events.member?.getSender()}
ts={member.events.member?.getTs()}
/>
<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>
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;
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 '';
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[];
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]);
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';
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);
);
}
-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);
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;
}>();
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(
);
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 =
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,
knockSupported,
restrictedSupported,
} from '../../../utils/matrix';
+import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
type RestrictedRoomAllowContent = {
room_id: string;
};
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());
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>();
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';
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;
}
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
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>();
-import React, { FormEventHandler, useCallback, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import {
Button,
color,
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) {
}
};
- 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}
replacementRoom
? tombstoneContent.body ||
`This ${room.isSpaceRoom() ? 'space' : 'room'} has been replaced!`
- : `Current room version: ${roomVersion}.`
+ : `Current version: ${roomVersion}.`
}
after={
<Box alignItems="Center" gap="200">
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>
</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>
);
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';
} 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';
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,
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);
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(
);
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,
getPermissionPower,
IPowerLevels,
PermissionLocation,
- usePowerLevelsAPI,
} from '../../../hooks/usePowerLevels';
import { PermissionGroup } from './types';
-import { getPowers, usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
+import { getPowerLevelTag, getPowers, usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { useRoom } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { StateEvent } from '../../../../types/matrix/room';
};
type PermissionGroupsProps = {
+ canEdit: boolean;
powerLevels: IPowerLevels;
permissionGroups: PermissionGroup[];
};
-export function PermissionGroups({ powerLevels, permissionGroups }: PermissionGroupsProps) {
+export function PermissionGroups({
+ powerLevels,
+ permissionGroups,
+ canEdit,
+}: PermissionGroupsProps) {
const mx = useMatrixClient();
const room = useRoom();
const alive = useAlive();
- const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
- const canChangePermission = canSendStateEvent(
- StateEvent.RoomPowerLevels,
- getPowerLevel(mx.getSafeUserId())
- );
- const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
+
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
const maxPower = useMemo(() => Math.max(...getPowers(powerLevelTags)), [powerLevelTags]);
const [permissionUpdate, setPermissionUpdate] = useState<Map<PermissionLocation, number>>(
permissionUpdate.forEach((power, location) =>
applyPermissionPower(draftPowerLevels, location, power)
);
+
return draftPowerLevels;
});
await mx.sendStateEvent(room.roomId, StateEvent.RoomPowerLevels as any, editedPowerLevels);
const powerUpdate = permissionUpdate.get(USER_DEFAULT_LOCATION);
const value = powerUpdate ?? power;
- const tag = getPowerLevelTag(value);
+ const tag = getPowerLevelTag(powerLevelTags, value);
const powerChanges = value !== power;
return (
fill="Soft"
radii="Pill"
aria-selected={opened}
- disabled={!canChangePermission || applyingChanges}
+ disabled={!canEdit || applyingChanges}
after={
powerChanges && (
<Badge size="200" variant="Success" fill="Solid" radii="Pill" />
)
}
before={
- canChangePermission && (
+ canEdit && (
<Icon size="50" src={opened ? Icons.ChevronTop : Icons.ChevronBottom} />
)
}
const powerUpdate = permissionUpdate.get(item.location);
const value = powerUpdate ?? power;
- const tag = getPowerLevelTag(value);
+ const tag = getPowerLevelTag(powerLevelTags, value);
const powerChanges = value !== power;
return (
fill="Soft"
radii="Pill"
aria-selected={opened}
- disabled={!canChangePermission || applyingChanges}
+ disabled={!canEdit || applyingChanges}
after={
powerChanges && (
<Badge size="200" variant="Success" fill="Solid" radii="Pill" />
)
}
before={
- canChangePermission && (
+ canEdit && (
<Icon
size="50"
src={opened ? Icons.ChevronTop : Icons.ChevronBottom}
} from 'folds';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
-import { getPowers, getTagIconSrc, usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
+import { getPowers, usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { SettingTile } from '../../../components/setting-tile';
import { getPermissionPower, IPowerLevels } from '../../../hooks/usePowerLevels';
import { useRoom } from '../../../hooks/useRoom';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { stopPropagation } from '../../../utils/keyboard';
import { PermissionGroup } from './types';
+import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
+import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
type PeekPermissionsProps = {
powerLevels: IPowerLevels;
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const room = useRoom();
- const [powerLevelTags] = usePowerLevelTags(room, powerLevels);
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const creators = useRoomCreators(room);
+ const creatorsTag = useRoomCreatorsTag();
+ const creatorTagIconSrc =
+ creatorsTag.icon && getPowerTagIconSrc(mx, useAuthentication, creatorsTag.icon);
return (
<Box direction="Column" gap="100">
+ {creators.size > 0 && (
+ <SequenceCard
+ variant="SurfaceVariant"
+ className={SequenceCardStyle}
+ direction="Column"
+ gap="400"
+ >
+ <SettingTile
+ title="Founders"
+ description="Founding members has all permissions and can only be changed during upgrade."
+ />
+
+ <SettingTile>
+ <Box gap="200" wrap="Wrap">
+ <Chip
+ disabled
+ variant="Secondary"
+ radii="300"
+ before={<PowerColorBadge color={creatorsTag.color} />}
+ after={creatorTagIconSrc && <PowerIcon size="50" iconSrc={creatorTagIconSrc} />}
+ >
+ <Text size="T300" truncate>
+ <b>{creatorsTag.name}</b>
+ </Text>
+ </Chip>
+ </Box>
+ </SettingTile>
+ </SequenceCard>
+ )}
<SequenceCard
variant="SurfaceVariant"
className={SequenceCardStyle}
<Box gap="200" wrap="Wrap">
{getPowers(powerLevelTags).map((power) => {
const tag = powerLevelTags[power];
- const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon);
+ const tagIconSrc = tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon);
return (
<PeekPermissions
import { SettingTile } from '../../../components/setting-tile';
import {
getPowers,
- getTagIconSrc,
getUsedPowers,
- PowerLevelTag,
- PowerLevelTagIcon,
PowerLevelTags,
usePowerLevelTags,
} from '../../../hooks/usePowerLevelTags';
import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
-import { StateEvent } from '../../../../types/matrix/room';
+import { MemberPowerTag, MemberPowerTagIcon, StateEvent } from '../../../../types/matrix/room';
import { useAlive } from '../../../hooks/useAlive';
import { BetaNoticeBadge } from '../../../components/BetaNoticeBadge';
+import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
+import { creatorsSupported } from '../../../utils/matrix';
type EditPowerProps = {
maxPower: number;
power?: number;
- tag?: PowerLevelTag;
- onSave: (power: number, tag: PowerLevelTag) => void;
+ tag?: MemberPowerTag;
+ onSave: (power: number, tag: MemberPowerTag) => void;
onClose: () => void;
};
function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
const room = useRoom();
const roomToParents = useAtomValue(roomToParentsAtom);
const useAuthentication = useMediaAuthentication();
+ const supportCreators = creatorsSupported(room.getVersion());
const imagePackRooms = useImagePackRooms(room.roomId, roomToParents);
const pickFile = useFilePicker(setIconFile, false);
const [tagColor, setTagColor] = useState<string | undefined>(tag?.color);
- const [tagIcon, setTagIcon] = useState<PowerLevelTagIcon | undefined>(tag?.icon);
+ const [tagIcon, setTagIcon] = useState<MemberPowerTagIcon | undefined>(tag?.icon);
const uploadingIcon = iconFile && !tagIcon;
- const tagIconSrc = tagIcon && getTagIconSrc(mx, useAuthentication, tagIcon);
+ const tagIconSrc = tagIcon && getPowerTagIconSrc(mx, useAuthentication, tagIcon);
const iconUploadAtom = useMemo(() => {
if (iconFile) return createUploadAtom(iconFile);
const tagPower = parseInt(powerInput.value, 10);
if (Number.isNaN(tagPower)) return;
- if (tagPower > maxPower) return;
+
const tagName = nameInput.value.trim();
if (!tagName) return;
- const editedTag: PowerLevelTag = {
+ const editedTag: MemberPowerTag = {
name: tagName,
color: tagColor,
icon: tagIcon,
radii="300"
type="number"
placeholder="75"
- max={maxPower}
+ max={supportCreators ? undefined : maxPower}
outlined={typeof power === 'number'}
readOnly={typeof power === 'number'}
required
return [up, Math.max(...Array.from(up))];
}, [powerLevels]);
- const [powerLevelTags] = usePowerLevelTags(room, powerLevels);
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
const [editedPowerTags, setEditedPowerTags] = useState<PowerLevelTags>();
const [deleted, setDeleted] = useState<Set<number>>(new Set());
}, []);
const handleSaveTag = useCallback(
- (power: number, tag: PowerLevelTag) => {
+ (power: number, tag: MemberPowerTag) => {
setEditedPowerTags((tags) => {
const editedTags = { ...(tags ?? powerLevelTags) };
editedTags[power] = tag;
</SequenceCard>
{getPowers(powerTags).map((power) => {
const tag = powerTags[power];
- const tagIconSrc = tag.icon && getTagIconSrc(mx, useAuthentication, tag.icon);
+ const tagIconSrc =
+ tag.icon && getPowerTagIconSrc(mx, useAuthentication, tag.icon);
return (
<SequenceCard
-import React, { FormEventHandler, useCallback, useState } from 'react';
+import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
import { MatrixError, Room } from 'matrix-js-sdk';
import {
Box,
} from 'folds';
import { SettingTile } from '../../components/setting-tile';
import { SequenceCard } from '../../components/sequence-card';
-import { knockRestrictedSupported, knockSupported, restrictedSupported } from '../../utils/matrix';
+import {
+ creatorsSupported,
+ knockRestrictedSupported,
+ knockSupported,
+ restrictedSupported,
+} from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { millisecondsToMinutes, replaceSpaceWithDash } from '../../utils/common';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useAlive } from '../../hooks/useAlive';
import { ErrorCode } from '../../cs-errorcode';
import {
+ AdditionalCreatorInput,
createRoom,
CreateRoomAliasInput,
CreateRoomData,
CreateRoomKind,
CreateRoomKindSelector,
RoomVersionSelector,
+ useAdditionalCreators,
} from '../../components/create-room';
const getCreateRoomKindToIcon = (kind: CreateRoomKind) => {
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 allowRestricted = space && restrictedSupported(selectedRoomVersion);
const [kind, setKind] = useState(
defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
);
+ const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
+ const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
+ useAdditionalCreators();
const [federation, setFederation] = useState(true);
const [encryption, setEncryption] = useState(false);
const [knock, setKnock] = useState(false);
encryption: publicRoom ? false : encryption,
knock: roomKnock,
allowFederation: federation,
+ additionalCreators: allowAdditionalCreators ? additionalCreators : undefined,
}).then((roomId) => {
if (alive()) {
onCreate?.(roomId);
</Chip>
</Box>
</Box>
+ {allowAdditionalCreators && (
+ <SequenceCard
+ style={{ padding: config.space.S300 }}
+ variant="SurfaceVariant"
+ direction="Column"
+ gap="500"
+ >
+ <AdditionalCreatorInput
+ additionalCreators={additionalCreators}
+ onSelect={addAdditionalCreator}
+ onRemove={removeAdditionalCreator}
+ />
+ </SequenceCard>
+ )}
{kind !== CreateRoomKind.Public && (
<>
<SequenceCard
-import React, { FormEventHandler, useCallback, useState } from 'react';
+import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
import { MatrixError, Room } from 'matrix-js-sdk';
import {
Box,
} from 'folds';
import { SettingTile } from '../../components/setting-tile';
import { SequenceCard } from '../../components/sequence-card';
-import { knockRestrictedSupported, knockSupported, restrictedSupported } from '../../utils/matrix';
+import {
+ creatorsSupported,
+ knockRestrictedSupported,
+ knockSupported,
+ restrictedSupported,
+} from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { millisecondsToMinutes, replaceSpaceWithDash } from '../../utils/common';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { useAlive } from '../../hooks/useAlive';
import { ErrorCode } from '../../cs-errorcode';
import {
+ AdditionalCreatorInput,
createRoom,
CreateRoomAliasInput,
CreateRoomData,
CreateRoomKind,
CreateRoomKindSelector,
RoomVersionSelector,
+ useAdditionalCreators,
} from '../../components/create-room';
import { RoomType } from '../../../types/matrix/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 allowRestricted = space && restrictedSupported(selectedRoomVersion);
const [kind, setKind] = useState(
defaultKind ?? allowRestricted ? CreateRoomKind.Restricted : CreateRoomKind.Private
);
+
+ const allowAdditionalCreators = creatorsSupported(selectedRoomVersion);
+ const { additionalCreators, addAdditionalCreator, removeAdditionalCreator } =
+ useAdditionalCreators();
const [federation, setFederation] = useState(true);
const [knock, setKnock] = useState(false);
const [advance, setAdvance] = useState(false);
aliasLocalPart: publicRoom ? aliasLocalPart : undefined,
knock: roomKnock,
allowFederation: federation,
+ additionalCreators: allowAdditionalCreators ? additionalCreators : undefined,
}).then((roomId) => {
if (alive()) {
onCreate?.(roomId);
</Chip>
</Box>
</Box>
+ {allowAdditionalCreators && (
+ <SequenceCard
+ style={{ padding: config.space.S300 }}
+ variant="SurfaceVariant"
+ direction="Column"
+ gap="500"
+ >
+ <AdditionalCreatorInput
+ additionalCreators={additionalCreators}
+ onSelect={addAdditionalCreator}
+ onRemove={removeAdditionalCreator}
+ />
+ </SequenceCard>
+ )}
{kind !== CreateRoomKind.Public && advance && (allowKnock || allowKnockRestricted) && (
<SequenceCard
style={{ padding: config.space.S300 }}
import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
+import { IPowerLevels } from '../../hooks/usePowerLevels';
+import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
+import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
type HierarchyItemWithParent = HierarchyItem & {
parentId: string;
const [toggleState, handleToggleSuggested] = useAsyncCallback(
useCallback(() => {
const newContent: MSpaceChildContent = { ...content, suggested: !content.suggested };
- return mx.sendStateEvent(parentId, StateEvent.SpaceChild, newContent, roomId);
+ return mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, newContent, roomId);
}, [mx, parentId, roomId, content])
);
const [removeState, handleRemove] = useAsyncCallback(
useCallback(
- () => mx.sendStateEvent(parentId, StateEvent.SpaceChild, {}, roomId),
+ () => mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, {}, roomId),
[mx, parentId, roomId]
)
);
parentId: string;
};
joined: boolean;
- canInvite: boolean;
+ powerLevels?: IPowerLevels;
canEditChild: boolean;
pinned?: boolean;
onTogglePin?: (roomId: string) => void;
export function HierarchyItemMenu({
item,
joined,
- canInvite,
+ powerLevels,
canEditChild,
pinned,
onTogglePin,
}: HierarchyItemMenuProps) {
+ const mx = useMatrixClient();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
+ const canInvite = (): boolean => {
+ if (!powerLevels) return false;
+ const creators = getRoomCreatorsForRoomId(mx, item.roomId);
+ const permissions = getRoomPermissionsAPI(creators, powerLevels);
+
+ return permissions.action('invite', mx.getSafeUserId());
+ };
+
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
<InviteMenuItem
item={item}
requestClose={handleRequestClose}
- disabled={!canInvite}
+ disabled={!canInvite()}
/>
<SettingsMenuItem item={item} requestClose={handleRequestClose} />
<UseStateProvider initial={false}>
import {
IPowerLevels,
PowerLevelsContextProvider,
- powerLevelAPI,
usePowerLevels,
useRoomsPowerLevels,
} from '../../hooks/usePowerLevels';
import { SpaceHierarchy } from './SpaceHierarchy';
import { useGetRoom } from '../../hooks/useGetRoom';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
+import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
+import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
const useCanDropLobbyItem = (
space: Room,
roomsPowerLevels: Map<string, IPowerLevels>,
- getRoom: (roomId: string) => Room | undefined,
- canEditSpaceChild: (powerLevels: IPowerLevels) => boolean
+ getRoom: (roomId: string) => Room | undefined
): CanDropCallback => {
const mx = useMatrixClient();
const containerSpaceId = space.roomId;
+ const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {};
+ const creators = getRoomCreatorsForRoomId(mx, containerSpaceId);
+ const permissions = getRoomPermissionsAPI(creators, powerLevels);
+
if (
getRoom(containerSpaceId) === undefined ||
- !canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
+ !permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
) {
return false;
}
return true;
},
- [space, roomsPowerLevels, getRoom, canEditSpaceChild]
+ [space, roomsPowerLevels, getRoom, mx]
);
const canDropRoom: CanDropCallback = useCallback(
// check and do not allow restricted room to be dragged outside
// current space if can't change `m.room.join_rules` `content.allow`
if (draggingOutsideSpace && restrictedItem) {
- const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
- const userPLInItem = powerLevelAPI.getPowerLevel(
- itemPowerLevel,
- mx.getUserId() ?? undefined
- );
- const canChangeJoinRuleAllow = powerLevelAPI.canSendStateEvent(
- itemPowerLevel,
+ const itemPowerLevels = roomsPowerLevels.get(item.roomId) ?? {};
+ const itemCreators = getRoomCreatorsForRoomId(mx, item.roomId);
+ const itemPermissions = getRoomPermissionsAPI(itemCreators, itemPowerLevels);
+
+ const canChangeJoinRuleAllow = itemPermissions.stateEvent(
StateEvent.RoomJoinRules,
- userPLInItem
+ mx.getSafeUserId()
);
if (!canChangeJoinRuleAllow) {
return false;
}
}
+ const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {};
+ const creators = getRoomCreatorsForRoomId(mx, containerSpaceId);
+ const permissions = getRoomPermissionsAPI(creators, powerLevels);
if (
getRoom(containerSpaceId) === undefined ||
- !canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
+ !permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
) {
return false;
}
return true;
},
- [mx, getRoom, canEditSpaceChild, roomsPowerLevels]
+ [mx, getRoom, roomsPowerLevels]
);
const canDrop: CanDropCallback = useCallback(
const getRoom = useGetRoom(allJoinedRooms);
- const canEditSpaceChild = useCallback(
- (powerLevels: IPowerLevels) =>
- powerLevelAPI.canSendStateEvent(
- powerLevels,
- StateEvent.SpaceChild,
- powerLevelAPI.getPowerLevel(powerLevels, mx.getUserId() ?? undefined)
- ),
- [mx]
- );
-
const [draggingItem, setDraggingItem] = useState<HierarchyItem>();
const hierarchy = useSpaceHierarchy(
space.roomId,
)
);
- const canDrop: CanDropCallback = useCanDropLobbyItem(
- space,
- roomsPowerLevels,
- getRoom,
- canEditSpaceChild
- );
+ const canDrop: CanDropCallback = useCanDropLobbyItem(space, roomsPowerLevels, getRoom);
const [reorderSpaceState, reorderSpace] = useAsyncCallback(
useCallback(
.filter((reorder, index) => {
if (!reorder.item.parentId) return false;
const parentPL = roomsPowerLevels.get(reorder.item.parentId);
- const canEdit = parentPL && canEditSpaceChild(parentPL);
+ if (!parentPL) return false;
+
+ const creators = getRoomCreatorsForRoomId(mx, reorder.item.parentId);
+ const permissions = getRoomPermissionsAPI(creators, parentPL);
+ const canEdit = permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId());
return canEdit && reorder.orderKey !== currentOrders[index];
});
});
}
},
- [mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild]
+ [mx, hierarchy, lex, roomsPowerLevels]
)
);
const reorderingSpace = reorderSpaceState.status === AsyncStatus.Loading;
newItems.push(rId);
}
const newSpacesContent = makeCinnySpacesContent(mx, newItems);
- mx.setAccountData(AccountDataEvent.CinnySpaces, newSpacesContent);
+ mx.setAccountData(AccountDataEvent.CinnySpaces as any, newSpacesContent as any);
},
[mx, sidebarItems, sidebarSpaces]
);
allJoinedRooms={allJoinedRooms}
mDirects={mDirects}
roomsPowerLevels={roomsPowerLevels}
- canEditSpaceChild={canEditSpaceChild}
categoryId={categoryId}
closed={
closedCategories.has(categoryId) ||
import { nameInitials } from '../../utils/common';
import * as css from './LobbyHeader.css';
import { openInviteUser } from '../../../client/action/navigation';
-import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
+import { IPowerLevels } from '../../hooks/usePowerLevels';
import { UseStateProvider } from '../../components/UseStateProvider';
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
import { stopPropagation } from '../../utils/keyboard';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../hooks/useRoomPermissions';
type LobbyMenuProps = {
- roomId: string;
powerLevels: IPowerLevels;
requestClose: () => void;
};
const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
- ({ roomId, powerLevels, requestClose }, ref) => {
+ ({ powerLevels, requestClose }, ref) => {
const mx = useMatrixClient();
- const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
- const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
+ const space = useSpace();
+ const creators = useRoomCreators(space);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canInvite = permissions.action('invite', mx.getSafeUserId());
const openSpaceSettings = useOpenSpaceSettings();
const handleInvite = () => {
- openInviteUser(roomId);
+ openInviteUser(space.roomId);
requestClose();
};
const handleRoomSettings = () => {
- openSpaceSettings(roomId);
+ openSpaceSettings(space.roomId);
requestClose();
};
</MenuItem>
{promptLeave && (
<LeaveSpacePrompt
- roomId={roomId}
+ roomId={space.roomId}
onDone={requestClose}
onCancel={() => setPromptLeave(false)}
/>
}}
>
<LobbyMenu
- roomId={space.roomId}
powerLevels={powerLevels}
requestClose={() => setMenuAnchor(undefined)}
/>
HierarchyItemSpace,
useFetchSpaceHierarchyLevel,
} from '../../hooks/useSpaceHierarchy';
-import { IPowerLevels, powerLevelAPI } from '../../hooks/usePowerLevels';
+import { IPowerLevels } from '../../hooks/usePowerLevels';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { SpaceItemCard } from './SpaceItem';
import { AfterItemDropTarget, CanDropCallback } from './DnD';
import { HierarchyItemMenu } from './HierarchyItemMenu';
import { RoomItemCard } from './RoomItem';
-import { RoomType } from '../../../types/matrix/room';
+import { RoomType, StateEvent } from '../../../types/matrix/room';
import { SequenceCard } from '../../components/sequence-card';
+import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
+import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
type SpaceHierarchyProps = {
summary: IHierarchyRoom | undefined;
allJoinedRooms: Set<string>;
mDirects: Set<string>;
roomsPowerLevels: Map<string, IPowerLevels>;
- canEditSpaceChild: (powerLevels: IPowerLevels) => boolean;
categoryId: string;
closed: boolean;
handleClose: MouseEventHandler<HTMLButtonElement>;
allJoinedRooms,
mDirects,
roomsPowerLevels,
- canEditSpaceChild,
categoryId,
closed,
handleClose,
return s;
}, [rooms]);
- const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId) ?? {};
- const userPLInSpace = powerLevelAPI.getPowerLevel(
- spacePowerLevels,
- mx.getUserId() ?? undefined
- );
- const canInviteInSpace = powerLevelAPI.canDoAction(spacePowerLevels, 'invite', userPLInSpace);
+ const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId);
+ const spaceCreators = getRoomCreatorsForRoomId(mx, spaceItem.roomId);
+ const spacePermissions =
+ spacePowerLevels && getRoomPermissionsAPI(spaceCreators, spacePowerLevels);
const draggingSpace =
draggingItem?.roomId === spaceItem.roomId && draggingItem.parentId === spaceItem.parentId;
const { parentId } = spaceItem;
- const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) ?? {} : undefined;
+ const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) : undefined;
+ const parentCreators = parentId ? getRoomCreatorsForRoomId(mx, parentId) : undefined;
+ const parentPermissions =
+ parentCreators &&
+ parentPowerLevels &&
+ getRoomPermissionsAPI(parentCreators, parentPowerLevels);
useEffect(() => {
onSpacesFound(Array.from(subspaces.values()));
}, [subspaces, onSpacesFound]);
let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId));
- if (!canEditSpaceChild(spacePowerLevels)) {
+ if (!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())) {
// hide unknown rooms for normal user
childItems = childItems?.filter((i) => {
const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false;
closed={closed}
handleClose={handleClose}
getRoom={getRoom}
- canEditChild={canEditSpaceChild(spacePowerLevels)}
+ canEditChild={!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())}
canReorder={
- parentPowerLevels && !disabledReorder ? canEditSpaceChild(parentPowerLevels) : false
+ parentPowerLevels && !disabledReorder && parentPermissions
+ ? parentPermissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
+ : false
}
options={
parentId &&
parentPowerLevels && (
<HierarchyItemMenu
item={{ ...spaceItem, parentId }}
- canInvite={canInviteInSpace}
+ powerLevels={spacePowerLevels}
joined={allJoinedRooms.has(spaceItem.roomId)}
- canEditChild={canEditSpaceChild(parentPowerLevels)}
+ canEditChild={
+ !!parentPermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
+ }
pinned={pinned}
onTogglePin={togglePinToSidebar}
/>
const roomSummary = rooms.get(roomItem.roomId);
const roomPowerLevels = roomsPowerLevels.get(roomItem.roomId) ?? {};
- const userPLInRoom = powerLevelAPI.getPowerLevel(
- roomPowerLevels,
- mx.getUserId() ?? undefined
- );
- const canInviteInRoom = powerLevelAPI.canDoAction(
- roomPowerLevels,
- 'invite',
- userPLInRoom
- );
const lastItem = index === childItems.length;
const nextRoomId = lastItem ? nextSpaceId : childItems[index + 1]?.roomId;
dm={mDirects.has(roomItem.roomId)}
onOpen={onOpenRoom}
getRoom={getRoom}
- canReorder={canEditSpaceChild(spacePowerLevels) && !disabledReorder}
+ canReorder={
+ !!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) &&
+ !disabledReorder
+ }
options={
<HierarchyItemMenu
item={roomItem}
- canInvite={canInviteInRoom}
+ powerLevels={roomPowerLevels}
joined={allJoinedRooms.has(roomItem.roomId)}
- canEditChild={canEditSpaceChild(spacePowerLevels)}
+ canEditChild={
+ !!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
+ }
/>
}
after={
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
-import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
-import {
- getTagIconSrc,
- useAccessibleTagColors,
- usePowerLevelTags,
-} from '../../hooks/usePowerLevelTags';
+import { usePowerLevels } from '../../hooks/usePowerLevels';
+import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
import { useTheme } from '../../hooks/useTheme';
import { PowerIcon } from '../../components/power';
import colorMXID from '../../../util/colorMXID';
+import {
+ getPowerTagIconSrc,
+ useAccessiblePowerTagColors,
+ useGetMemberPowerTag,
+} from '../../hooks/useMemberPowerTag';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
+import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
type SearchResultGroupProps = {
room: Room;
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
const powerLevels = usePowerLevels(room);
- const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
- const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
+ const creators = useRoomCreators(room);
+
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+
const theme = useTheme();
- const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
+ const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
const threadRootId =
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
- const senderPowerLevel = getPowerLevel(event.sender);
- const powerLevelTag = getPowerLevelTag(senderPowerLevel);
- const tagColor = powerLevelTag?.color
- ? accessibleTagColors?.get(powerLevelTag.color)
+ const memberPowerTag = getMemberPowerTag(event.sender);
+ const tagColor = memberPowerTag?.color
+ ? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
- const tagIconSrc = powerLevelTag?.icon
- ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
+ const tagIconSrc = memberPowerTag?.icon
+ ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenClick}
- getPowerLevel={getPowerLevel}
- getPowerLevelTag={getPowerLevelTag}
+ getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor}
/>
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
-import { usePowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
+import { usePowerLevels } from '../../hooks/usePowerLevels';
import { copyToClipboard } from '../../utils/dom';
import { markAsRead } from '../../../client/action/notifications';
import { openInviteUser } from '../../../client/action/navigation';
RoomNotificationMode,
} from '../../hooks/useRoomsNotificationPreferences';
import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../hooks/useRoomPermissions';
type RoomNavItemMenuProps = {
room: Room;
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
- const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
- const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canInvite = permissions.action('invite', mx.getSafeUserId());
const openRoomSettings = useOpenRoomSettings();
const space = useSpaceOptionally();
RoomPublish,
RoomUpgrade,
} from '../../common-settings/general';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type GeneralProps = {
requestClose: () => void;
export function General({ requestClose }: GeneralProps) {
const room = useRoom();
const powerLevels = usePowerLevels(room);
+ const creators = useRoomCreators(room);
+ const permissions = useRoomPermissions(creators, powerLevels);
return (
<Page>
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
- <RoomProfile powerLevels={powerLevels} />
+ <RoomProfile permissions={permissions} />
<Box direction="Column" gap="100">
<Text size="L400">Options</Text>
- <RoomJoinRules powerLevels={powerLevels} />
- <RoomHistoryVisibility powerLevels={powerLevels} />
- <RoomEncryption powerLevels={powerLevels} />
- <RoomPublish powerLevels={powerLevels} />
+ <RoomJoinRules permissions={permissions} />
+ <RoomHistoryVisibility permissions={permissions} />
+ <RoomEncryption permissions={permissions} />
+ <RoomPublish permissions={permissions} />
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Addresses</Text>
- <RoomPublishedAddresses powerLevels={powerLevels} />
- <RoomLocalAddresses powerLevels={powerLevels} />
+ <RoomPublishedAddresses permissions={permissions} />
+ <RoomLocalAddresses permissions={permissions} />
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Advance Options</Text>
- <RoomUpgrade powerLevels={powerLevels} requestClose={requestClose} />
+ <RoomUpgrade permissions={permissions} requestClose={requestClose} />
</Box>
</Box>
</PageContent>
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom';
-import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { StateEvent } from '../../../../types/matrix/room';
import { usePermissionGroups } from './usePermissionItems';
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type PermissionsProps = {
requestClose: () => void;
const mx = useMatrixClient();
const room = useRoom();
const powerLevels = usePowerLevels(room);
- const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
- const canEditPowers = canSendStateEvent(
- StateEvent.PowerLevelTags,
- getPowerLevel(mx.getSafeUserId())
- );
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+
+ const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
+ const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
const permissionGroups = usePermissionGroups();
const [powerEditor, setPowerEditor] = useState(false);
onEdit={canEditPowers ? handleEditPowers : undefined}
permissionGroups={permissionGroups}
/>
- <PermissionGroups powerLevels={powerLevels} permissionGroups={permissionGroups} />
+ <PermissionGroups
+ canEdit={canEditPermissions}
+ powerLevels={powerLevels}
+ permissionGroups={permissionGroups}
+ />
</Box>
</PageContent>
</Scroll>
import { keyframes, style } from '@vanilla-extract/css';
-import { color, config, toRem } from 'folds';
+import { config, toRem } from 'folds';
export const MembersDrawer = style({
width: toRem(266),
- backgroundColor: color.Background.Container,
- color: color.Background.OnContainer,
});
export const MembersDrawerHeader = style({
TooltipProvider,
config,
} from 'folds';
-import { Room, RoomMember } from 'matrix-js-sdk';
+import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import { useVirtualizer } from '@tanstack/react-virtual';
import classNames from 'classnames';
useAsyncSearch,
} from '../../hooks/useAsyncSearch';
import { useDebounce } from '../../hooks/useDebounce';
-import { usePowerLevelTags, useFlattenPowerLevelTagMembers } from '../../hooks/usePowerLevelTags';
import { TypingIndicator } from '../../components/typing-indicator';
import { getMemberDisplayName, getMemberSearchStr } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix';
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter';
-import { useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort';
-import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
+import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort';
+import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { MembershipFilterMenu } from '../../components/MembershipFilterMenu';
import { MemberSortMenu } from '../../components/MemberSortMenu';
import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../hooks/useSpace';
+import { ContainerColor } from '../../styles/ContainerColor.css';
+import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
+
+type MemberDrawerHeaderProps = {
+ room: Room;
+};
+function MemberDrawerHeader({ room }: MemberDrawerHeaderProps) {
+ const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
+
+ return (
+ <Header className={css.MembersDrawerHeader} variant="Background" size="600">
+ <Box grow="Yes" alignItems="Center" gap="200">
+ <Box grow="Yes" alignItems="Center" gap="200">
+ <Text title={`${room.getJoinedMemberCount()} Members`} size="H5" truncate>
+ {`${millify(room.getJoinedMemberCount())} Members`}
+ </Text>
+ </Box>
+ <Box shrink="No" alignItems="Center">
+ <TooltipProvider
+ position="Bottom"
+ align="End"
+ offset={4}
+ tooltip={
+ <Tooltip>
+ <Text>Close</Text>
+ </Tooltip>
+ }
+ >
+ {(triggerRef) => (
+ <IconButton
+ ref={triggerRef}
+ variant="Background"
+ onClick={() => setPeopleDrawer(false)}
+ >
+ <Icon src={Icons.Cross} />
+ </IconButton>
+ )}
+ </TooltipProvider>
+ </Box>
+ </Box>
+ </Header>
+ );
+}
+
+type MemberItemProps = {
+ mx: MatrixClient;
+ useAuthentication: boolean;
+ room: Room;
+ member: RoomMember;
+ onClick: MouseEventHandler<HTMLButtonElement>;
+ pressed?: boolean;
+ typing?: boolean;
+};
+function MemberItem({
+ mx,
+ useAuthentication,
+ room,
+ member,
+ onClick,
+ pressed,
+ typing,
+}: MemberItemProps) {
+ const name =
+ getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
+ const avatarMxcUrl = member.getMxcAvatarUrl();
+ const avatarUrl = avatarMxcUrl
+ ? mx.mxcUrlToHttp(avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication)
+ : undefined;
+
+ return (
+ <MenuItem
+ style={{ padding: `0 ${config.space.S200}` }}
+ aria-pressed={pressed}
+ data-user-id={member.userId}
+ variant="Background"
+ radii="400"
+ onClick={onClick}
+ before={
+ <Avatar size="200">
+ <UserAvatar
+ userId={member.userId}
+ src={avatarUrl ?? undefined}
+ alt={name}
+ renderFallback={() => <Icon size="50" src={Icons.User} filled />}
+ />
+ </Avatar>
+ }
+ after={
+ typing && (
+ <Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
+ <TypingIndicator size="300" />
+ </Badge>
+ )
+ }
+ >
+ <Box grow="Yes">
+ <Text size="T400" truncate>
+ {name}
+ </Text>
+ </Box>
+ </MenuItem>
+ );
+}
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
const searchInputRef = useRef<HTMLInputElement>(null);
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
const powerLevels = usePowerLevelsContext();
- const [, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
+ const creators = useRoomCreators(room);
+ const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+
const fetchingMembers = members.length < room.getJoinedMemberCount();
- const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
const openUserRoomProfile = useOpenUserRoomProfile();
const space = useSpaceOptionally();
const openProfileUserId = useUserRoomProfileState()?.userId;
const sortFilterMenu = useMemberSortMenu();
const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex');
const [membershipFilterIndex, setMembershipFilterIndex] = useState(0);
- const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu);
const memberSort = useMemberSort(sortFilterIndex, sortFilterMenu);
+ const memberPowerSort = useMemberPowerSort(creators);
const typingMembers = useRoomTypingMember(room.roomId);
const filteredMembers = useMemo(
- () =>
- members
- .filter(membershipFilter.filterFn)
- .sort(memberSort.sortFn)
- .sort((a, b) => b.powerLevel - a.powerLevel),
- [members, membershipFilter, memberSort]
+ () => members.filter(membershipFilter.filterFn).sort(memberSort.sortFn).sort(memberPowerSort),
+ [members, membershipFilter, memberSort, memberPowerSort]
);
const [result, search, resetSearch] = useAsyncSearch(
const processMembers = result ? result.items : filteredMembers;
- const PLTagOrRoomMember = useFlattenPowerLevelTagMembers(
- processMembers,
- getPowerLevel,
- getPowerLevelTag
- );
+ const PLTagOrRoomMember = useFlattenPowerTagMembers(processMembers, getPowerTag);
const virtualizer = useVirtualizer({
count: PLTagOrRoomMember.length,
{ wait: 200 }
);
- const getName = (member: RoomMember) =>
- getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
-
const handleMemberClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
const btn = evt.currentTarget as HTMLButtonElement;
const userId = btn.getAttribute('data-user-id');
};
return (
- <Box className={css.MembersDrawer} shrink="No" direction="Column">
- <Header className={css.MembersDrawerHeader} variant="Background" size="600">
- <Box grow="Yes" alignItems="Center" gap="200">
- <Box grow="Yes" alignItems="Center" gap="200">
- <Text title={`${room.getJoinedMemberCount()} Members`} size="H5" truncate>
- {`${millify(room.getJoinedMemberCount())} Members`}
- </Text>
- </Box>
- <Box shrink="No" alignItems="Center">
- <TooltipProvider
- position="Bottom"
- align="End"
- offset={4}
- tooltip={
- <Tooltip>
- <Text>Close</Text>
- </Tooltip>
- }
- >
- {(triggerRef) => (
- <IconButton
- ref={triggerRef}
- variant="Background"
- onClick={() => setPeopleDrawer(false)}
- >
- <Icon src={Icons.Cross} />
- </IconButton>
- )}
- </TooltipProvider>
- </Box>
- </Box>
- </Header>
+ <Box
+ className={classNames(css.MembersDrawer, ContainerColor({ variant: 'Background' }))}
+ shrink="No"
+ direction="Column"
+ >
+ <MemberDrawerHeader room={room} />
<Box className={css.MemberDrawerContentBase} grow="Yes">
<Scroll ref={scrollRef} variant="Background" size="300" visibility="Hover" hideTrack>
<Box className={css.MemberDrawerContent} direction="Column" gap="200">
);
}
- const member = tagOrMember;
- const name = getName(member);
- const avatarMxcUrl = member.getMxcAvatarUrl();
- const avatarUrl = avatarMxcUrl
- ? mx.mxcUrlToHttp(
- avatarMxcUrl,
- 100,
- 100,
- 'crop',
- undefined,
- false,
- useAuthentication
- )
- : undefined;
-
return (
- <MenuItem
+ <div
style={{
- padding: `0 ${config.space.S200}`,
transform: `translateY(${vItem.start}px)`,
}}
- aria-pressed={openProfileUserId === member.userId}
+ className={css.DrawerVirtualItem}
data-index={vItem.index}
- data-user-id={member.userId}
+ key={`${room.roomId}-${tagOrMember.userId}`}
ref={virtualizer.measureElement}
- key={`${room.roomId}-${member.userId}`}
- className={css.DrawerVirtualItem}
- variant="Background"
- radii="400"
- onClick={handleMemberClick}
- before={
- <Avatar size="200">
- <UserAvatar
- userId={member.userId}
- src={avatarUrl ?? undefined}
- alt={name}
- renderFallback={() => <Icon size="50" src={Icons.User} filled />}
- />
- </Avatar>
- }
- after={
- typingMembers.find((receipt) => receipt.userId === member.userId) && (
- <Badge size="300" variant="Secondary" fill="Soft" radii="Pill" outlined>
- <TypingIndicator size="300" />
- </Badge>
- )
- }
>
- <Box grow="Yes">
- <Text size="T400" truncate>
- {name}
- </Text>
- </Box>
- </MenuItem>
+ <MemberItem
+ mx={mx}
+ useAuthentication={useAuthentication}
+ room={room}
+ member={tagOrMember}
+ onClick={handleMemberClick}
+ pressed={openProfileUserId === tagOrMember.userId}
+ typing={typingMembers.some(
+ (receipt) => receipt.userId === tagOrMember.userId
+ )}
+ />
+ </div>
);
})}
</div>
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
-import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
-import { powerLevelAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
+import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import colorMXID from '../../../util/colorMXID';
import { useIsDirectRoom } from '../../hooks/useRoom';
+import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
+import { useTheme } from '../../hooks/useTheme';
+import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
+import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
interface RoomInputProps {
editor: Editor;
fileDropContainerRef: RefObject<HTMLElement>;
roomId: string;
room: Room;
- getPowerLevelTag: GetPowerLevelTag;
- accessibleTagColors: Map<string, string>;
}
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
- ({ editor, fileDropContainerRef, roomId, room, getPowerLevelTag, accessibleTagColors }, ref) => {
+ ({ editor, fileDropContainerRef, roomId, room }, ref) => {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const emojiBtnRef = useRef<HTMLButtonElement>(null);
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevelsContext();
+ const creators = useRoomCreators(room);
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
const replyUserID = replyDraft?.userId;
- const replyPowerTag = getPowerLevelTag(powerLevelAPI.getPowerLevel(powerLevels, replyUserID));
- const replyPowerColor = replyPowerTag.color
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const creatorsTag = useRoomCreatorsTag();
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+ const theme = useTheme();
+ const accessibleTagColors = useAccessiblePowerTagColors(
+ theme.kind,
+ creatorsTag,
+ powerLevelTags
+ );
+
+ const replyPowerTag = replyUserID ? getMemberPowerTag(replyUserID) : undefined;
+ const replyPowerColor = replyPowerTag?.color
? accessibleTagColors.get(replyPowerTag.color)
: undefined;
const replyUsernameColor =
});
handleCancelUpload(uploads);
const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises));
- contents.forEach((content) => mx.sendMessage(roomId, content));
+ contents.forEach((content) => mx.sendMessage(roomId, content as any));
};
const submit = useCallback(() => {
content['m.relates_to'].is_falling_back = false;
}
}
- mx.sendMessage(roomId, content);
+ mx.sendMessage(roomId, content as any);
resetEditor(editor);
resetEditorHistory(editor);
setReplyDraft(undefined);
import { inSameDay, minuteDifference, timeDayMonthYear, today, yesterday } from '../../utils/time';
import { createMentionElement, isEmptyEditor, moveCursor } from '../../components/editor';
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
-import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
+import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { GetContentCallback, MessageEvent, StateEvent } from '../../../types/matrix/room';
import { useKeyDown } from '../../hooks/useKeyDown';
import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
-import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
import { useIsDirectRoom } from '../../hooks/useRoom';
import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { useSpaceOptionally } from '../../hooks/useSpace';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../hooks/useRoomPermissions';
+import { useAccessiblePowerTagColors, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
+import { useTheme } from '../../hooks/useTheme';
+import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
+import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
const TimelineFloat = as<'div', css.TimelineFloatVariants>(
({ position, className, ...props }, ref) => (
eventId?: string;
roomInputRef: RefObject<HTMLElement>;
editor: Editor;
- getPowerLevelTag: GetPowerLevelTag;
- accessibleTagColors: Map<string, string>;
};
const PAGINATION_LIMIT = 80;
};
};
-export function RoomTimeline({
- room,
- eventId,
- roomInputRef,
- editor,
- getPowerLevelTag,
- accessibleTagColors,
-}: RoomTimelineProps) {
+export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const setReplyDraft = useSetAtom(roomIdToReplyDraftAtomFamily(room.roomId));
const powerLevels = usePowerLevelsContext();
- const { canDoAction, canSendEvent, canSendStateEvent, getPowerLevel } =
- usePowerLevelsAPI(powerLevels);
+ const creators = useRoomCreators(room);
- const myPowerLevel = getPowerLevel(mx.getUserId() ?? '');
- const canRedact = canDoAction('redact', myPowerLevel);
- const canSendReaction = canSendEvent(MessageEvent.Reaction, myPowerLevel);
- const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, myPowerLevel);
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+
+ const theme = useTheme();
+ const accessiblePowerTagColors = useAccessiblePowerTagColors(
+ theme.kind,
+ creatorsTag,
+ powerLevelTags
+ );
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+
+ const canRedact = permissions.action('redact', mx.getSafeUserId());
+ const canSendReaction = permissions.event(MessageEvent.Reaction, mx.getSafeUserId());
+ const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, mx.getSafeUserId());
const [editId, setEditId] = useState<string>();
const roomToParents = useAtomValue(roomToParentsAtom);
(reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined);
mx.sendEvent(
room.roomId,
- MessageEvent.Reaction,
+ MessageEvent.Reaction as any,
getReactionContent(targetEventId, key, rShortcode)
);
},
editedEvent?.getContent()['m.new_content'] ?? mEvent.getContent()) as GetContentCallback;
const senderId = mEvent.getSender() ?? '';
- const senderPowerLevel = getPowerLevel(mEvent.getSender());
const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenReply}
- getPowerLevel={getPowerLevel}
- getPowerLevelTag={getPowerLevelTag}
- accessibleTagColors={accessibleTagColors}
+ getMemberPowerTag={getMemberPowerTag}
+ accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
/>
)
}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
- powerLevelTag={getPowerLevelTag(senderPowerLevel)}
- accessibleTagColors={accessibleTagColors}
+ memberPowerTag={getMemberPowerTag(senderId)}
+ accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
const hasReactions = reactions && reactions.length > 0;
const { replyEventId, threadRootId } = mEvent;
const highlighted = focusItem?.index === item && focusItem.highlight;
- const senderPowerLevel = getPowerLevel(mEvent.getSender());
return (
<Message
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenReply}
- getPowerLevel={getPowerLevel}
- getPowerLevelTag={getPowerLevelTag}
- accessibleTagColors={accessibleTagColors}
+ getMemberPowerTag={getMemberPowerTag}
+ accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
/>
)
}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
- powerLevelTag={getPowerLevelTag(senderPowerLevel)}
- accessibleTagColors={accessibleTagColors}
+ memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
+ accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
const hasReactions = reactions && reactions.length > 0;
const highlighted = focusItem?.index === item && focusItem.highlight;
- const senderPowerLevel = getPowerLevel(mEvent.getSender());
return (
<Message
}
hideReadReceipts={hideActivity}
showDeveloperTools={showDeveloperTools}
- powerLevelTag={getPowerLevelTag(senderPowerLevel)}
- accessibleTagColors={accessibleTagColors}
+ memberPowerTag={getMemberPowerTag(mEvent.getSender() ?? '')}
+ accessibleTagColors={accessiblePowerTagColors}
legacyUsernameColor={legacyUsernameColor || direct}
hour24Clock={hour24Clock}
dateFormatString={dateFormatString}
import { isKeyHotkey } from 'is-hotkey';
import { useStateEvent } from '../../hooks/useStateEvent';
import { StateEvent } from '../../../types/matrix/room';
-import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
+import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useEditor } from '../../components/editor';
import { RoomInputPlaceholder } from './RoomInputPlaceholder';
import navigation from '../../../client/state/navigation';
import { settingsAtom } from '../../state/settings';
import { useSetting } from '../../state/hooks/settings';
-import { useAccessibleTagColors, usePowerLevelTags } from '../../hooks/usePowerLevelTags';
-import { useTheme } from '../../hooks/useTheme';
+import { useRoomPermissions } from '../../hooks/useRoomPermissions';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
const tombstoneEvent = useStateEvent(room, StateEvent.RoomTombstone);
const powerLevels = usePowerLevelsContext();
- const { getPowerLevel, canSendEvent } = usePowerLevelsAPI(powerLevels);
- const myUserId = mx.getUserId();
- const canMessage = myUserId
- ? canSendEvent(EventType.RoomMessage, getPowerLevel(myUserId))
- : false;
+ const creators = useRoomCreators(room);
- const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
- const theme = useTheme();
- const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canMessage = permissions.event(EventType.RoomMessage, mx.getSafeUserId());
useKeyDown(
window,
eventId={eventId}
roomInputRef={roomInputRef}
editor={editor}
- getPowerLevelTag={getPowerLevelTag}
- accessibleTagColors={accessibleTagColors}
/>
<RoomViewTyping room={room} />
</Box>
roomId={roomId}
fileDropContainerRef={roomViewRef}
ref={roomInputRef}
- getPowerLevelTag={getPowerLevelTag}
- accessibleTagColors={accessibleTagColors}
/>
)}
{!canMessage && (
import { _SearchPathSearchParams } from '../../pages/paths';
import * as css from './RoomViewHeader.css';
import { useRoomUnread } from '../../state/hooks/unread';
-import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
+import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { markAsRead } from '../../../client/action/notifications';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { openInviteUser } from '../../../client/action/navigation';
} from '../../hooks/useRoomsNotificationPreferences';
import { JumpToTime } from './jump-to-time';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
+import { useRoomCreators } from '../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../hooks/useRoomPermissions';
type RoomMenuProps = {
room: Room;
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevelsContext();
- const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
- const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canInvite = permissions.action('invite', mx.getSafeUserId());
const notificationPreferences = useRoomsNotificationPreferencesContext();
const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId);
const { navigateRoom } = useRoomNavigate();
import { getViaServers } from '../../../plugins/via-servers';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { useRoomPinnedEvents } from '../../../hooks/useRoomPinnedEvents';
-import { StateEvent } from '../../../../types/matrix/room';
-import { getTagIconSrc, PowerLevelTag } from '../../../hooks/usePowerLevelTags';
+import { MemberPowerTag, StateEvent } from '../../../../types/matrix/room';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
+import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
if (!isPinned && eventId) {
pinContent.pinned.push(eventId);
}
- mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents, pinContent);
+ mx.sendStateEvent(room.roomId, StateEvent.RoomPinnedEvents as any, pinContent);
onClose?.();
};
reactions?: ReactNode;
hideReadReceipts?: boolean;
showDeveloperTools?: boolean;
- powerLevelTag?: PowerLevelTag;
+ memberPowerTag?: MemberPowerTag;
accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean;
hour24Clock: boolean;
reactions,
hideReadReceipts,
showDeveloperTools,
- powerLevelTag,
+ memberPowerTag,
accessibleTagColors,
legacyUsernameColor,
hour24Clock,
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
const senderAvatarMxc = getMemberAvatarMxc(room, senderId);
- const tagColor = powerLevelTag?.color
- ? accessibleTagColors?.get(powerLevelTag.color)
+ const tagColor = memberPowerTag?.color
+ ? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
- const tagIconSrc = powerLevelTag?.icon
- ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
+ const tagIconSrc = memberPowerTag?.icon
+ ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(senderId) : tagColor;
import { ImageViewer } from '../../../components/image-viewer';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { VirtualTile } from '../../../components/virtualizer';
-import { usePowerLevelsAPI, usePowerLevelsContext } from '../../../hooks/usePowerLevels';
+import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { ContainerColor } from '../../../styles/ContainerColor.css';
-import {
- getTagIconSrc,
- useAccessibleTagColors,
- usePowerLevelTags,
-} from '../../../hooks/usePowerLevelTags';
+import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { useTheme } from '../../../hooks/useTheme';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
import { useIsDirectRoom } from '../../../hooks/useRoom';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
+import {
+ GetMemberPowerTag,
+ getPowerTagIconSrc,
+ useAccessiblePowerTagColors,
+ useGetMemberPowerTag,
+} from '../../../hooks/useMemberPowerTag';
+import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
type PinnedMessageProps = {
room: Room;
renderContent: RenderMatrixEvent<[MatrixEvent, string, GetContentCallback]>;
onOpen: (roomId: string, eventId: string) => void;
canPinEvent: boolean;
+ getMemberPowerTag: GetMemberPowerTag;
+ accessibleTagColors: Map<string, string>;
+ legacyUsernameColor: boolean;
+ hour24Clock: boolean;
+ dateFormatString: string;
};
-function PinnedMessage({ room, eventId, renderContent, onOpen, canPinEvent }: PinnedMessageProps) {
+function PinnedMessage({
+ room,
+ eventId,
+ renderContent,
+ onOpen,
+ canPinEvent,
+ getMemberPowerTag,
+ accessibleTagColors,
+ legacyUsernameColor,
+ hour24Clock,
+ dateFormatString,
+}: PinnedMessageProps) {
const pinnedEvent = useRoomEvent(room, eventId);
const useAuthentication = useMediaAuthentication();
const mx = useMatrixClient();
- const direct = useIsDirectRoom();
- const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
-
- const powerLevels = usePowerLevelsContext();
- const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
- const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
- const theme = useTheme();
- const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
-
- const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
- const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
const [unpinState, unpin] = useAsyncCallback(
useCallback(() => {
const senderAvatarMxc = getMemberAvatarMxc(room, sender);
const getContent = (() => pinnedEvent.getContent()) as GetContentCallback;
- const senderPowerLevel = getPowerLevel(sender);
- const powerLevelTag = getPowerLevelTag(senderPowerLevel);
- const tagColor = powerLevelTag?.color ? accessibleTagColors?.get(powerLevelTag.color) : undefined;
- const tagIconSrc = powerLevelTag?.icon
- ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
+ const memberPowerTag = getMemberPowerTag(sender);
+ const tagColor = memberPowerTag?.color
+ ? accessibleTagColors?.get(memberPowerTag.color)
+ : undefined;
+ const tagIconSrc = memberPowerTag?.icon
+ ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
- const usernameColor = legacyUsernameColor || direct ? colorMXID(sender) : tagColor;
+ const usernameColor = legacyUsernameColor ? colorMXID(sender) : tagColor;
return (
<ModernLayout
replyEventId={pinnedEvent.replyEventId}
threadRootId={pinnedEvent.threadRootId}
onClick={handleOpenClick}
- getPowerLevel={getPowerLevel}
- getPowerLevelTag={getPowerLevelTag}
+ getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor}
/>
const mx = useMatrixClient();
const userId = mx.getUserId()!;
const powerLevels = usePowerLevelsContext();
- const { canSendStateEvent, getPowerLevel } = usePowerLevelsAPI(powerLevels);
- const canPinEvent = canSendStateEvent(StateEvent.RoomPinnedEvents, getPowerLevel(userId));
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, userId);
+
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+
+ const theme = useTheme();
+ const accessibleTagColors = useAccessiblePowerTagColors(
+ theme.kind,
+ creatorsTag,
+ powerLevelTags
+ );
const pinnedEvents = useRoomPinnedEvents(room);
const sortedPinnedEvent = useMemo(() => Array.from(pinnedEvents).reverse(), [pinnedEvents]);
const useAuthentication = useMediaAuthentication();
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
+
+ const direct = useIsDirectRoom();
+ const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
+
+ const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock');
+ const [dateFormatString] = useSetting(settingsAtom, 'dateFormatString');
+
const { navigateRoom } = useRoomNavigate();
const scrollRef = useRef<HTMLDivElement>(null);
renderContent={renderMatrixEvent}
onOpen={handleOpen}
canPinEvent={canPinEvent}
+ getMemberPowerTag={getMemberPowerTag}
+ accessibleTagColors={accessibleTagColors}
+ legacyUsernameColor={legacyUsernameColor || direct}
+ hour24Clock={hour24Clock}
+ dateFormatString={dateFormatString}
/>
</SequenceCard>
</VirtualTile>
RoomPublish,
RoomUpgrade,
} from '../../common-settings/general';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type GeneralProps = {
requestClose: () => void;
export function General({ requestClose }: GeneralProps) {
const room = useRoom();
const powerLevels = usePowerLevels(room);
+ const creators = useRoomCreators(room);
+ const permissions = useRoomPermissions(creators, powerLevels);
return (
<Page>
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
- <RoomProfile powerLevels={powerLevels} />
+ <RoomProfile permissions={permissions} />
<Box direction="Column" gap="100">
<Text size="L400">Options</Text>
- <RoomJoinRules powerLevels={powerLevels} />
- <RoomPublish powerLevels={powerLevels} />
+ <RoomJoinRules permissions={permissions} />
+ <RoomPublish permissions={permissions} />
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Addresses</Text>
- <RoomPublishedAddresses powerLevels={powerLevels} />
- <RoomLocalAddresses powerLevels={powerLevels} />
+ <RoomPublishedAddresses permissions={permissions} />
+ <RoomLocalAddresses permissions={permissions} />
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Advance Options</Text>
- <RoomUpgrade powerLevels={powerLevels} requestClose={requestClose} />
+ <RoomUpgrade permissions={permissions} requestClose={requestClose} />
</Box>
</Box>
</PageContent>
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { useRoom } from '../../../hooks/useRoom';
-import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { StateEvent } from '../../../../types/matrix/room';
import { usePermissionGroups } from './usePermissionItems';
import { PermissionGroups, Powers, PowersEditor } from '../../common-settings/permissions';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type PermissionsProps = {
requestClose: () => void;
const mx = useMatrixClient();
const room = useRoom();
const powerLevels = usePowerLevels(room);
- const { getPowerLevel, canSendStateEvent } = usePowerLevelsAPI(powerLevels);
- const canEditPowers = canSendStateEvent(
- StateEvent.PowerLevelTags,
- getPowerLevel(mx.getSafeUserId())
- );
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+
+ const canEditPowers = permissions.stateEvent(StateEvent.PowerLevelTags, mx.getSafeUserId());
+ const canEditPermissions = permissions.stateEvent(StateEvent.RoomPowerLevels, mx.getSafeUserId());
const permissionGroups = usePermissionGroups();
const [powerEditor, setPowerEditor] = useState(false);
onEdit={canEditPowers ? handleEditPowers : undefined}
permissionGroups={permissionGroups}
/>
- <PermissionGroups powerLevels={powerLevels} permissionGroups={permissionGroups} />
+ <PermissionGroups
+ canEdit={canEditPermissions}
+ powerLevels={powerLevels}
+ permissionGroups={permissionGroups}
+ />
</Box>
</PageContent>
</Scroll>
--- /dev/null
+import { useMemo } from 'react';
+import { AccountDataEvent, MDirectContent } from '../../types/matrix/accountData';
+import { useAccountData } from './useAccountData';
+import { useAllJoinedRoomsSet, useGetRoom } from './useGetRoom';
+
+export const useDirectUsers = (): string[] => {
+ const directEvent = useAccountData(AccountDataEvent.Direct);
+ const content = directEvent?.getContent<MDirectContent>();
+
+ const allJoinedRooms = useAllJoinedRoomsSet();
+ const getRoom = useGetRoom(allJoinedRooms);
+
+ const users = useMemo(() => {
+ if (typeof content !== 'object') return [];
+
+ const u = Object.keys(content).filter((userId) => {
+ const rooms = content[userId];
+ if (!Array.isArray(rooms)) return false;
+ const hasDM = rooms.some((roomId) => typeof roomId === 'string' && !!getRoom(roomId));
+ return hasDM;
+ });
+
+ return u;
+ }, [content, getRoom]);
+
+ return users;
+};
--- /dev/null
+import { useCallback } from 'react';
+import { IPowerLevels, readPowerLevel } from './usePowerLevels';
+
+export const useMemberPowerCompare = (creators: Set<string>, powerLevels: IPowerLevels) => {
+ /**
+ * returns `true` if `userIdA` has more power than `userIdB`
+ * returns `false` otherwise
+ */
+ const hasMorePower = useCallback(
+ (userIdA: string, userIdB: string): boolean => {
+ const aIsCreator = creators.has(userIdA);
+ const bIsCreator = creators.has(userIdB);
+ if (aIsCreator && bIsCreator) return false;
+ if (aIsCreator) return true;
+ if (bIsCreator) return false;
+
+ const aPower = readPowerLevel.user(powerLevels, userIdA);
+ const bPower = readPowerLevel.user(powerLevels, userIdB);
+
+ return aPower > bPower;
+ },
+ [creators, powerLevels]
+ );
+
+ return {
+ hasMorePower,
+ };
+};
--- /dev/null
+import { useCallback, useMemo } from 'react';
+import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
+import { getPowerLevelTag, PowerLevelTags, usePowerLevelTags } from './usePowerLevelTags';
+import { IPowerLevels, readPowerLevel } from './usePowerLevels';
+import { MemberPowerTag, MemberPowerTagIcon } from '../../types/matrix/room';
+import { useRoomCreatorsTag } from './useRoomCreatorsTag';
+import { ThemeKind } from './useTheme';
+import { accessibleColor } from '../plugins/color';
+
+export type GetMemberPowerTag = (userId: string) => MemberPowerTag;
+
+export const useGetMemberPowerTag = (
+ room: Room,
+ creators: Set<string>,
+ powerLevels: IPowerLevels
+) => {
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+
+ const getMemberPowerTag: GetMemberPowerTag = useCallback(
+ (userId) => {
+ if (creators.has(userId)) {
+ return creatorsTag;
+ }
+
+ const power = readPowerLevel.user(powerLevels, userId);
+ return getPowerLevelTag(powerLevelTags, power);
+ },
+ [creators, creatorsTag, powerLevels, powerLevelTags]
+ );
+
+ return getMemberPowerTag;
+};
+
+export const getPowerTagIconSrc = (
+ mx: MatrixClient,
+ useAuthentication: boolean,
+ icon: MemberPowerTagIcon
+): string | undefined =>
+ icon?.key?.startsWith('mxc://')
+ ? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
+ : icon?.key;
+
+export const useAccessiblePowerTagColors = (
+ themeKind: ThemeKind,
+ creatorsTag: MemberPowerTag,
+ powerLevelTags: PowerLevelTags
+): Map<string, string> => {
+ const accessibleColors: Map<string, string> = useMemo(() => {
+ const colors: Map<string, string> = new Map();
+ if (creatorsTag.color) {
+ colors.set(creatorsTag.color, accessibleColor(themeKind, creatorsTag.color));
+ }
+
+ Object.values(powerLevelTags).forEach((tag) => {
+ const { color } = tag;
+ if (!color) return;
+
+ colors.set(color, accessibleColor(themeKind, color));
+ });
+
+ return colors;
+ }, [powerLevelTags, creatorsTag, themeKind]);
+
+ return accessibleColors;
+};
+
+export const useFlattenPowerTagMembers = (
+ members: RoomMember[],
+ getTag: GetMemberPowerTag
+): Array<MemberPowerTag | RoomMember> => {
+ const PLTagOrRoomMember = useMemo(() => {
+ let prevTag: MemberPowerTag | undefined;
+ const tagOrMember: Array<MemberPowerTag | RoomMember> = [];
+ members.forEach((member) => {
+ const tag = getTag(member.userId);
+ if (tag !== prevTag) {
+ prevTag = tag;
+ tagOrMember.push(tag);
+ }
+ tagOrMember.push(member);
+ });
+ return tagOrMember;
+ }, [members, getTag]);
+
+ return PLTagOrRoomMember;
+};
import { RoomMember } from 'matrix-js-sdk';
-import { useMemo } from 'react';
+import { useCallback, useMemo } from 'react';
export const MemberSort = {
Ascending: (a: RoomMember, b: RoomMember) =>
const item = memberSort[index] ?? memberSort[0];
return item;
};
+
+export const useMemberPowerSort = (creators: Set<string>): MemberSortFn => {
+ const sort: MemberSortFn = useCallback(
+ (a, b) => {
+ if (creators.has(a.userId) && creators.has(b.userId)) {
+ return 0;
+ }
+ if (creators.has(a.userId)) return -1;
+ if (creators.has(b.userId)) return 1;
+
+ return b.powerLevel - a.powerLevel;
+ },
+ [creators]
+ );
+
+ return sort;
+};
-import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
-import { useCallback, useMemo } from 'react';
+import { Room } from 'matrix-js-sdk';
+import { useMemo } from 'react';
import { IPowerLevels } from './usePowerLevels';
import { useStateEvent } from './useStateEvent';
-import { StateEvent } from '../../types/matrix/room';
-import { IImageInfo } from '../../types/matrix/common';
-import { ThemeKind } from './useTheme';
-import { accessibleColor } from '../plugins/color';
-
-export type PowerLevelTagIcon = {
- key?: string;
- info?: IImageInfo;
-};
-export type PowerLevelTag = {
- name: string;
- color?: string;
- icon?: PowerLevelTagIcon;
-};
+import { MemberPowerTag, StateEvent } from '../../types/matrix/room';
-export type PowerLevelTags = Record<number, PowerLevelTag>;
+export type PowerLevelTags = Record<number, MemberPowerTag>;
-export const powerSortFn = (a: number, b: number) => b - a;
-export const sortPowers = (powers: number[]): number[] => powers.sort(powerSortFn);
+const powerSortFn = (a: number, b: number) => b - a;
+const sortPowers = (powers: number[]): number[] => powers.sort(powerSortFn);
export const getPowers = (tags: PowerLevelTags): number[] => {
- const powers: number[] = Object.keys(tags).map((p) => parseInt(p, 10));
+ const powers: number[] = Object.keys(tags)
+ .map((p) => {
+ const power = parseInt(p, 10);
+ if (Number.isNaN(power)) {
+ return undefined;
+ }
+ return power;
+ })
+ .filter((power) => typeof power === 'number');
return sortPowers(powers);
};
name: 'Goku',
color: '#ff6a00',
},
- 102: {
- name: 'Goku Reborn',
+ 150: {
+ name: 'Co-Founder',
color: '#ff6a7f',
},
101: {
},
};
-const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): PowerLevelTag => {
+const generateFallbackTag = (powerLevelTags: PowerLevelTags, power: number): MemberPowerTag => {
const highToLow = sortPowers(getPowers(powerLevelTags));
const tagPower = highToLow.find((p) => p < power);
};
};
-export type GetPowerLevelTag = (powerLevel: number) => PowerLevelTag;
-
-export const usePowerLevelTags = (
- room: Room,
- powerLevels: IPowerLevels
-): [PowerLevelTags, GetPowerLevelTag] => {
+export const usePowerLevelTags = (room: Room, powerLevels: IPowerLevels): PowerLevelTags => {
const tagsEvent = useStateEvent(room, StateEvent.PowerLevelTags);
const powerLevelTags: PowerLevelTags = useMemo(() => {
return powerToTags;
}, [powerLevels, tagsEvent]);
- const getTag: GetPowerLevelTag = useCallback(
- (power) => {
- const tag: PowerLevelTag | undefined = powerLevelTags[power];
- return tag ?? generateFallbackTag(DEFAULT_TAGS, power);
- },
- [powerLevelTags]
- );
-
- return [powerLevelTags, getTag];
+ return powerLevelTags;
};
-export const useFlattenPowerLevelTagMembers = (
- members: RoomMember[],
- getPowerLevel: (userId: string) => number,
- getTag: GetPowerLevelTag
-): Array<PowerLevelTag | RoomMember> => {
- const PLTagOrRoomMember = useMemo(() => {
- let prevTag: PowerLevelTag | undefined;
- const tagOrMember: Array<PowerLevelTag | RoomMember> = [];
- members.forEach((member) => {
- const memberPL = getPowerLevel(member.userId);
- const tag = getTag(memberPL);
- if (tag !== prevTag) {
- prevTag = tag;
- tagOrMember.push(tag);
- }
- tagOrMember.push(member);
- });
- return tagOrMember;
- }, [members, getTag, getPowerLevel]);
-
- return PLTagOrRoomMember;
-};
-
-export const getTagIconSrc = (
- mx: MatrixClient,
- useAuthentication: boolean,
- icon: PowerLevelTagIcon
-): string | undefined =>
- icon?.key?.startsWith('mxc://')
- ? mx.mxcUrlToHttp(icon.key, 96, 96, 'scale', undefined, undefined, useAuthentication) ?? '🌻'
- : icon?.key;
-
-export const useAccessibleTagColors = (
- themeKind: ThemeKind,
- powerLevelTags: PowerLevelTags
-): Map<string, string> => {
- const accessibleColors: Map<string, string> = useMemo(() => {
- const colors: Map<string, string> = new Map();
-
- getPowers(powerLevelTags).forEach((power) => {
- const tag = powerLevelTags[power];
- const { color } = tag;
- if (!color) return;
-
- colors.set(color, accessibleColor(themeKind, color));
- });
-
- return colors;
- }, [powerLevelTags, themeKind]);
-
- return accessibleColors;
+export const getPowerLevelTag = (
+ powerLevelTags: PowerLevelTags,
+ powerLevel: number
+): MemberPowerTag => {
+ const tag: MemberPowerTag | undefined = powerLevelTags[powerLevel];
+ return tag ?? generateFallbackTag(powerLevelTags, powerLevel);
};
});
const getPowersLevelFromMatrixEvent = (mEvent?: MatrixEvent): IPowerLevels => {
- const pl = mEvent?.getContent<IPowerLevels>();
- if (!pl) return DEFAULT_POWER_LEVELS;
+ const plContent = mEvent?.getContent<IPowerLevels>();
- return fillMissingPowers(pl);
+ const powerLevels = !plContent ? DEFAULT_POWER_LEVELS : fillMissingPowers(plContent);
+
+ return powerLevels;
};
export function usePowerLevels(room: Room): IPowerLevels {
return roomToPowerLevels;
};
-export type GetPowerLevel = (powerLevels: IPowerLevels, userId: string | undefined) => number;
-export type CanSend = (
- powerLevels: IPowerLevels,
- eventType: string | undefined,
- powerLevel: number
-) => boolean;
-export type CanDoAction = (
- powerLevels: IPowerLevels,
- action: PowerLevelActions,
- powerLevel: number
-) => boolean;
-export type CanDoNotificationAction = (
- powerLevels: IPowerLevels,
- action: PowerLevelNotificationsAction,
- powerLevel: number
-) => boolean;
-
-export type PowerLevelsAPI = {
- getPowerLevel: GetPowerLevel;
- canSendEvent: CanSend;
- canSendStateEvent: CanSend;
- canDoAction: CanDoAction;
- canDoNotificationAction: CanDoNotificationAction;
-};
-
export type ReadPowerLevelAPI = {
- user: GetPowerLevel;
+ user: (powerLevels: IPowerLevels, userId: string | undefined) => number;
event: (powerLevels: IPowerLevels, eventType: string | undefined) => number;
state: (powerLevels: IPowerLevels, eventType: string | undefined) => number;
action: (powerLevels: IPowerLevels, action: PowerLevelActions) => number;
export const readPowerLevel: ReadPowerLevelAPI = {
user: (powerLevels, userId) => {
const { users_default: usersDefault, users } = powerLevels;
+
if (userId && users && typeof users[userId] === 'number') {
return users[userId];
}
},
};
-export const powerLevelAPI: PowerLevelsAPI = {
- getPowerLevel: (powerLevels, userId) => readPowerLevel.user(powerLevels, userId),
- canSendEvent: (powerLevels, eventType, powerLevel) => {
- const requiredPL = readPowerLevel.event(powerLevels, eventType);
- return powerLevel >= requiredPL;
- },
- canSendStateEvent: (powerLevels, eventType, powerLevel) => {
- const requiredPL = readPowerLevel.state(powerLevels, eventType);
- return powerLevel >= requiredPL;
- },
- canDoAction: (powerLevels, action, powerLevel) => {
- const requiredPL = readPowerLevel.action(powerLevels, action);
- return powerLevel >= requiredPL;
- },
- canDoNotificationAction: (powerLevels, action, powerLevel) => {
- const requiredPL = readPowerLevel.notification(powerLevels, action);
- return powerLevel >= requiredPL;
- },
-};
-
-export const usePowerLevelsAPI = (powerLevels: IPowerLevels) => {
- const getPowerLevel = useCallback(
- (userId: string | undefined) => powerLevelAPI.getPowerLevel(powerLevels, userId),
- [powerLevels]
- );
-
- const canSendEvent = useCallback(
- (eventType: string | undefined, powerLevel: number) =>
- powerLevelAPI.canSendEvent(powerLevels, eventType, powerLevel),
- [powerLevels]
- );
-
- const canSendStateEvent = useCallback(
- (eventType: string | undefined, powerLevel: number) =>
- powerLevelAPI.canSendStateEvent(powerLevels, eventType, powerLevel),
- [powerLevels]
- );
-
- const canDoAction = useCallback(
- (action: PowerLevelActions, powerLevel: number) =>
- powerLevelAPI.canDoAction(powerLevels, action, powerLevel),
- [powerLevels]
- );
-
- const canDoNotificationAction = useCallback(
- (action: PowerLevelNotificationsAction, powerLevel: number) =>
- powerLevelAPI.canDoNotificationAction(powerLevels, action, powerLevel),
+export const useGetMemberPowerLevel = (powerLevels: IPowerLevels) => {
+ const callback = useCallback(
+ (userId?: string): number => readPowerLevel.user(powerLevels, userId),
[powerLevels]
);
- return {
- getPowerLevel,
- canSendEvent,
- canSendStateEvent,
- canDoAction,
- canDoNotificationAction,
- };
+ return callback;
};
/**
--- /dev/null
+import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
+import { useMemo } from 'react';
+import { useStateEvent } from './useStateEvent';
+import { IRoomCreateContent, StateEvent } from '../../types/matrix/room';
+import { creatorsSupported } from '../utils/matrix';
+import { getStateEvent } from '../utils/room';
+
+export const getRoomCreators = (createEvent: MatrixEvent): Set<string> => {
+ const createContent = createEvent.getContent<IRoomCreateContent>();
+
+ const creators: Set<string> = new Set();
+
+ if (!creatorsSupported(createContent.room_version)) return creators;
+
+ if (createEvent.event.sender) {
+ creators.add(createEvent.event.sender);
+ }
+
+ if ('additional_creators' in createContent && Array.isArray(createContent.additional_creators)) {
+ createContent.additional_creators.forEach((creator) => {
+ if (typeof creator === 'string') {
+ creators.add(creator);
+ }
+ });
+ }
+
+ return creators;
+};
+
+export const useRoomCreators = (room: Room): Set<string> => {
+ const createEvent = useStateEvent(room, StateEvent.RoomCreate);
+
+ const creators = useMemo(
+ () => (createEvent ? getRoomCreators(createEvent) : new Set<string>()),
+ [createEvent]
+ );
+
+ return creators;
+};
+
+export const getRoomCreatorsForRoomId = (mx: MatrixClient, roomId: string): Set<string> => {
+ const room = mx.getRoom(roomId);
+ if (!room) return new Set();
+
+ const createEvent = getStateEvent(room, StateEvent.RoomCreate);
+ if (!createEvent) return new Set();
+
+ return getRoomCreators(createEvent);
+};
--- /dev/null
+import { MemberPowerTag } from '../../types/matrix/room';
+
+const DEFAULT_TAG: MemberPowerTag = {
+ name: 'Founder',
+ color: '#0000ff',
+};
+
+export const useRoomCreatorsTag = (): MemberPowerTag => DEFAULT_TAG;
--- /dev/null
+import { useMemo } from 'react';
+import {
+ IPowerLevels,
+ PowerLevelActions,
+ PowerLevelNotificationsAction,
+ readPowerLevel,
+} from './usePowerLevels';
+
+export type RoomPermissionsAPI = {
+ event: (type: string, userId: string) => boolean;
+ stateEvent: (type: string, userId: string) => boolean;
+ action: (action: PowerLevelActions, userId: string) => boolean;
+ notificationAction: (action: PowerLevelNotificationsAction, userId: string) => boolean;
+};
+
+export const getRoomPermissionsAPI = (
+ creators: Set<string>,
+ powerLevels: IPowerLevels
+): RoomPermissionsAPI => {
+ const api: RoomPermissionsAPI = {
+ event: (type, userId) => {
+ if (creators.has(userId)) return true;
+ const userPower = readPowerLevel.user(powerLevels, userId);
+ const requiredPL = readPowerLevel.event(powerLevels, type);
+ return userPower >= requiredPL;
+ },
+ stateEvent: (type, userId) => {
+ if (creators.has(userId)) return true;
+ const userPower = readPowerLevel.user(powerLevels, userId);
+ const requiredPL = readPowerLevel.state(powerLevels, type);
+ return userPower >= requiredPL;
+ },
+ action: (action, userId) => {
+ if (creators.has(userId)) return true;
+ const userPower = readPowerLevel.user(powerLevels, userId);
+ const requiredPL = readPowerLevel.action(powerLevels, action);
+ return userPower >= requiredPL;
+ },
+ notificationAction: (action, userId) => {
+ if (creators.has(userId)) return true;
+ const userPower = readPowerLevel.user(powerLevels, userId);
+ const requiredPL = readPowerLevel.notification(powerLevels, action);
+ return userPower >= requiredPL;
+ },
+ };
+
+ return api;
+};
+
+export const useRoomPermissions = (
+ creators: Set<string>,
+ powerLevels: IPowerLevels
+): RoomPermissionsAPI => {
+ const api: RoomPermissionsAPI = useMemo(
+ () => getRoomPermissionsAPI(creators, powerLevels),
+ [creators, powerLevels]
+ );
+
+ return api;
+};
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { allRoomsAtom } from '../../../state/room-list/roomList';
-import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
-import {
- getTagIconSrc,
- useAccessibleTagColors,
- usePowerLevelTags,
-} from '../../../hooks/usePowerLevelTags';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
+import { usePowerLevelTags } from '../../../hooks/usePowerLevelTags';
import { useTheme } from '../../../hooks/useTheme';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
import { mDirectAtom } from '../../../state/mDirectList';
+import {
+ getPowerTagIconSrc,
+ useAccessiblePowerTagColors,
+ useGetMemberPowerTag,
+} from '../../../hooks/useMemberPowerTag';
+import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
type RoomNotificationsGroup = {
roomId: string;
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
- const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
- const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
+ const creators = useRoomCreators(room);
+
+ const creatorsTag = useRoomCreatorsTag();
+ const powerLevelTags = usePowerLevelTags(room, powerLevels);
+ const getMemberPowerTag = useGetMemberPowerTag(room, creators, powerLevels);
+
const theme = useTheme();
- const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
+ const accessibleTagColors = useAccessiblePowerTagColors(theme.kind, creatorsTag, powerLevelTags);
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
const threadRootId =
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
- const senderPowerLevel = getPowerLevel(event.sender);
- const powerLevelTag = getPowerLevelTag(senderPowerLevel);
- const tagColor = powerLevelTag?.color
- ? accessibleTagColors?.get(powerLevelTag.color)
+ const memberPowerTag = getMemberPowerTag(event.sender);
+ const tagColor = memberPowerTag?.color
+ ? accessibleTagColors?.get(memberPowerTag.color)
: undefined;
- const tagIconSrc = powerLevelTag?.icon
- ? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
+ const tagIconSrc = memberPowerTag?.icon
+ ? getPowerTagIconSrc(mx, useAuthentication, memberPowerTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenClick}
- getPowerLevel={getPowerLevel}
- getPowerLevelTag={getPowerLevelTag}
+ getMemberPowerTag={getMemberPowerTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor}
/>
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
import { useOpenedSidebarFolderAtom } from '../../../state/hooks/openedSidebarFolder';
-import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { useRoomsUnread } from '../../../state/hooks/unread';
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
import { markAsRead } from '../../../../client/action/notifications';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
type SpaceMenuProps = {
room: Room;
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
- const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
- const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canInvite = permissions.action('invite', mx.getSafeUserId());
const openSpaceSettings = useOpenSpaceSettings();
const allChild = useSpaceChildren(
import {
Avatar,
Box,
+ Button,
Icon,
IconButton,
Icons,
MenuItem,
PopOut,
RectCords,
+ Spinner,
Text,
+ color,
config,
toRem,
} from 'folds';
import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
-import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
+import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { openInviteUser } from '../../../../client/action/navigation';
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../../state/hooks/roomList';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { copyToClipboard } from '../../../utils/dom';
import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories';
import { useStateEvent } from '../../../hooks/useStateEvent';
-import { StateEvent } from '../../../../types/matrix/room';
+import { Membership, StateEvent } from '../../../../types/matrix/room';
import { stopPropagation } from '../../../utils/keyboard';
import { getMatrixToRoom } from '../../../plugins/matrix-to';
import { getViaServers } from '../../../plugins/via-servers';
} from '../../../hooks/useRoomsNotificationPreferences';
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
+import { useRoomCreators } from '../../../hooks/useRoomCreators';
+import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
+import { ContainerColor } from '../../../styles/ContainerColor.css';
+import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
+import { BreakWord } from '../../../styles/Text.css';
type SpaceMenuProps = {
room: Room;
const [developerTools] = useSetting(settingsAtom, 'developerTools');
const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room);
- const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
- const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
+ const creators = useRoomCreators(room);
+
+ const permissions = useRoomPermissions(creators, powerLevels);
+ const canInvite = permissions.action('invite', mx.getSafeUserId());
const openSpaceSettings = useOpenSpaceSettings();
const { navigateRoom } = useRoomNavigate();
);
}
+type SpaceTombstoneProps = { roomId: string; replacementRoomId: string };
+export function SpaceTombstone({ roomId, replacementRoomId }: SpaceTombstoneProps) {
+ const mx = useMatrixClient();
+ const { navigateRoom } = useRoomNavigate();
+
+ const [joinState, handleJoin] = useAsyncCallback(
+ useCallback(() => {
+ const currentRoom = mx.getRoom(roomId);
+ const via = currentRoom ? getViaServers(currentRoom) : [];
+ return mx.joinRoom(replacementRoomId, {
+ viaServers: via,
+ });
+ }, [mx, roomId, replacementRoomId])
+ );
+ const replacementRoom = mx.getRoom(replacementRoomId);
+
+ const handleOpen = () => {
+ if (replacementRoom) navigateRoom(replacementRoom.roomId);
+ if (joinState.status === AsyncStatus.Success) navigateRoom(joinState.data.roomId);
+ };
+
+ return (
+ <Box
+ style={{
+ padding: config.space.S200,
+ borderRadius: config.radii.R400,
+ borderWidth: config.borderWidth.B300,
+ }}
+ className={ContainerColor({ variant: 'Surface' })}
+ direction="Column"
+ gap="300"
+ >
+ <Box direction="Column" grow="Yes" gap="100">
+ <Text size="L400">Space Upgraded</Text>
+ <Text size="T200">This space has been replaced and is no longer active.</Text>
+ {joinState.status === AsyncStatus.Error && (
+ <Text className={BreakWord} style={{ color: color.Critical.Main }} size="T200">
+ {(joinState.error as any)?.message ?? 'Failed to join replacement space!'}
+ </Text>
+ )}
+ </Box>
+ <Box direction="Column" shrink="No">
+ {replacementRoom?.getMyMembership() === Membership.Join ||
+ joinState.status === AsyncStatus.Success ? (
+ <Button onClick={handleOpen} size="300" variant="Success" fill="Solid" radii="300">
+ <Text size="B300">Open New Space</Text>
+ </Button>
+ ) : (
+ <Button
+ onClick={handleJoin}
+ size="300"
+ variant="Primary"
+ fill="Solid"
+ radii="300"
+ before={
+ joinState.status === AsyncStatus.Loading && (
+ <Spinner size="100" variant="Primary" fill="Solid" />
+ )
+ }
+ disabled={joinState.status === AsyncStatus.Loading}
+ >
+ <Text size="B300">Join New Space</Text>
+ </Button>
+ )}
+ </Box>
+ </Box>
+ );
+}
+
export function Space() {
const mx = useMatrixClient();
const space = useSpace();
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
const notificationPreferences = useRoomsNotificationPreferencesContext();
+ const tombstoneEvent = useStateEvent(space, StateEvent.RoomTombstone);
+
const selectedRoomId = useSelectedRoom();
const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias);
const searchSelected = useSpaceSearchSelected(spaceIdOrAlias);
<SpaceHeader />
<PageNavContent scrollRef={scrollRef}>
<Box direction="Column" gap="300">
+ {tombstoneEvent && (
+ <SpaceTombstone
+ roomId={space.roomId}
+ replacementRoomId={tombstoneEvent.getContent().replacement_room}
+ />
+ )}
<NavCategory>
<NavItem variant="Background" radii="400" aria-selected={lobbySelected}>
<NavLink to={getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, space.roomId))}>
const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
return !unsupportedVersion.includes(version);
};
+export const creatorsSupported = (version: string): boolean => {
+ const unsupportedVersion = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
+ return !unsupportedVersion.includes(version);
+};
MegolmBackupV1 = 'm.megolm_backup.v1',
}
+export type MDirectContent = Record<string, string[]>;
+
export type SecretStorageDefaultKeyContent = {
key: string;
};
+import { IImageInfo } from './common';
+
export enum Membership {
Invite = 'invite',
Knock = 'knock',
room_version: string;
type?: string;
predecessor?: {
- event_id: string;
+ event_id?: string;
room_id: string;
};
};
added: string[];
removed: string[];
};
+
+export type MemberPowerTagIcon = {
+ key?: string;
+ info?: IImageInfo;
+};
+export type MemberPowerTag = {
+ name: string;
+ color?: string;
+ icon?: MemberPowerTagIcon;
+};