add new sdk in general room settings

This commit is contained in:
Ajay Bura 2026-07-23 22:01:44 +05:30
commit e56eeace64
18 changed files with 580 additions and 446 deletions

View file

@ -13,44 +13,43 @@ import {
Button,
Spinner,
} from 'folds';
import { JoinRule } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { stopPropagation } from '../utils/keyboard';
import { getRoomIconSrc } from '../utils/room';
import { JoinRule } from '../../types/matrix/room';
export type ExtraJoinRules = 'knock_restricted';
export type ExtendedJoinRules = JoinRule | ExtraJoinRules;
type JoinRuleIcons = Record<ExtendedJoinRules, IconSrc>;
type JoinRuleIcons = Record<JoinRule, IconSrc>;
export const useJoinRuleIcons = (roomType?: string): JoinRuleIcons =>
useMemo(
() => ({
[JoinRule.Invite]: getRoomIconSrc(Icons, roomType, JoinRule.Invite),
[JoinRule.Knock]: getRoomIconSrc(Icons, roomType, JoinRule.Knock),
knock_restricted: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
[JoinRule.Restricted]: getRoomIconSrc(Icons, roomType, JoinRule.Restricted),
[JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public),
[JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private),
invite: getRoomIconSrc(Icons, roomType, 'invite'),
knock: getRoomIconSrc(Icons, roomType, 'knock'),
restricted: getRoomIconSrc(Icons, roomType, 'restricted'),
knock_restricted: getRoomIconSrc(Icons, roomType, 'knock_restricted'),
public: getRoomIconSrc(Icons, roomType, 'public'),
private: getRoomIconSrc(Icons, roomType, 'private'),
}),
[roomType]
);
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
type JoinRuleLabels = Record<JoinRule, string>;
export const useRoomJoinRuleLabel = (): JoinRuleLabels =>
useMemo(
() => ({
[JoinRule.Invite]: 'Invite Only',
[JoinRule.Knock]: 'Knock & Invite',
invite: 'Invite Only',
knock: 'Knock & Invite',
knock_restricted: 'Space Members or Knock',
[JoinRule.Restricted]: 'Space Members',
[JoinRule.Public]: 'Public',
[JoinRule.Private]: 'Invite Only',
restricted: 'Space Members',
public: 'Public',
private: 'Invite Only',
}),
[]
);
type JoinRulesSwitcherProps<T extends ExtendedJoinRules[]> = {
type JoinRulesSwitcherProps<T extends JoinRule[]> = {
icons: JoinRuleIcons;
labels: JoinRuleLabels;
rules: T;
@ -59,7 +58,7 @@ type JoinRulesSwitcherProps<T extends ExtendedJoinRules[]> = {
disabled?: boolean;
changing?: boolean;
};
export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
export function JoinRulesSwitcher<T extends JoinRule[]>({
icons,
labels,
rules,
@ -75,7 +74,7 @@ export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
};
const handleChange = useCallback(
(selectedRule: ExtendedJoinRules) => {
(selectedRule: JoinRule) => {
setCords(undefined);
onChange(selectedRule);
},
@ -127,7 +126,7 @@ export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
fill="Soft"
radii="300"
outlined
before={<Icon size="100" src={icons[value] ?? icons[JoinRule.Restricted]} />}
before={<Icon size="100" src={icons[value] ?? 'restricted'} />}
after={
changing ? (
<Spinner size="100" variant="Secondary" fill="Soft" />

View file

@ -14,11 +14,9 @@ import {
Text,
toRem,
} from 'folds';
import { MatrixError } from 'matrix-js-sdk';
import { SettingTile } from '../../../components/setting-tile';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRoom } from '../../../hooks/useRoom';
import {
useLocalAliases,
@ -30,25 +28,26 @@ import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { CutoutCard } from '../../../components/cutout-card';
import { replaceSpaceWithDash } from '../../../utils/common';
import { useAlive } from '../../../hooks/useAlive';
import { StateEvent } from '../../../../types/matrix/room';
import { StateEventType } from '../../../../types/matrix/room';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
import { getMxIdServer } from '../../../utils/matrix';
import { useClient } from '../../../hooks/useClient';
type RoomPublishedAddressesProps = {
permissions: RoomPermissionsAPI;
};
export function RoomPublishedAddresses({ permissions }: RoomPublishedAddressesProps) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = useRoom();
const canEditCanonical = permissions.stateEvent(
StateEvent.RoomCanonicalAlias,
mx.getSafeUserId()
StateEventType.RoomCanonicalAlias,
ct.session.userId
);
const [canonicalAlias, publishedAliases] = usePublishedAliases(room);
const setMainAlias = useSetMainAlias(room);
const [canonicalAlias, publishedAliases] = usePublishedAliases(roomId);
const setMainAlias = useSetMainAlias(roomId, publishedAliases);
const [mainState, setMain] = useAsyncCallback(setMainAlias);
const loading = mainState.status === AsyncStatus.Loading;
@ -120,7 +119,7 @@ export function RoomPublishedAddresses({ permissions }: RoomPublishedAddressesPr
{mainState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: color.Critical.Main }}>
{(mainState.error as MatrixError).message}
{mainState.error instanceof Error ? mainState.error.message : 'Unknown Error'}
</Text>
)}
</Box>
@ -131,8 +130,8 @@ export function RoomPublishedAddresses({ permissions }: RoomPublishedAddressesPr
}
function LocalAddressInput({ addLocalAlias }: { addLocalAlias: (alias: string) => Promise<void> }) {
const mx = useMatrixClient();
const userId = mx.getSafeUserId();
const ct = useClient();
const { userId } = ct.session;
const server = getMxIdServer(userId);
const alive = useAlive();
@ -189,9 +188,7 @@ function LocalAddressInput({ addLocalAlias }: { addLocalAlias: (alias: string) =
</Box>
{addState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T200">
{(addState.error as MatrixError).httpStatus === 409
? 'Address is already in use!'
: (addState.error as MatrixError).message}
{addState.error instanceof Error ? addState.error.message : 'Unknown Error'}
</Text>
)}
</Box>
@ -207,11 +204,15 @@ function LocalAddressesList({
removeLocalAlias: (alias: string) => Promise<void>;
canEditCanonical?: boolean;
}) {
const room = useRoom();
const roomId = useRoom();
const alive = useAlive();
const [, publishedAliases] = usePublishedAliases(room);
const { publishAliases, unpublishAliases } = usePublishUnpublishAliases(room);
const [canonicalAlias, publishedAliases] = usePublishedAliases(roomId);
const { publishAliases, unpublishAliases } = usePublishUnpublishAliases(
roomId,
publishedAliases,
canonicalAlias
);
const [selectedAliases, setSelectedAliases] = useState<string[]>([]);
const selectHasPublished = selectedAliases.find((alias) => publishedAliases.includes(alias));
@ -261,10 +262,10 @@ function LocalAddressesList({
deleteState.status === AsyncStatus.Loading ||
publishState.status === AsyncStatus.Loading ||
unpublishState.status === AsyncStatus.Loading;
let error: MatrixError | undefined;
if (deleteState.status === AsyncStatus.Error) error = deleteState.error as MatrixError;
if (publishState.status === AsyncStatus.Error) error = publishState.error as MatrixError;
if (unpublishState.status === AsyncStatus.Error) error = unpublishState.error as MatrixError;
let error: unknown;
if (deleteState.status === AsyncStatus.Error) error = deleteState.error;
if (publishState.status === AsyncStatus.Error) error = publishState.error;
if (unpublishState.status === AsyncStatus.Error) error = unpublishState.error;
return (
<Box direction="Column" gap="300">
@ -352,7 +353,7 @@ function LocalAddressesList({
})}
{error && (
<Text size="T200" style={{ color: color.Critical.Main }}>
{error.message}
{error instanceof Error ? error.message : 'Unknown Error'}
</Text>
)}
</Box>
@ -360,17 +361,17 @@ function LocalAddressesList({
}
export function RoomLocalAddresses({ permissions }: { permissions: RoomPermissionsAPI }) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = useRoom();
const canEditCanonical = permissions.stateEvent(
StateEvent.RoomCanonicalAlias,
mx.getSafeUserId()
StateEventType.RoomCanonicalAlias,
ct.session.userId
);
const [expand, setExpand] = useState(false);
const { localAliasesState, addLocalAlias, removeLocalAlias } = useLocalAliases(room.roomId);
const { localAliasesState, addLocalAlias, removeLocalAlias } = useLocalAliases(roomId);
return (
<SequenceCard
@ -424,7 +425,9 @@ export function RoomLocalAddresses({ permissions }: { permissions: RoomPermissio
{localAliasesState.status === AsyncStatus.Error && (
<Box gap="100">
<Text size="T200" style={{ color: color.Critical.Main }}>
{localAliasesState.error.message}
{localAliasesState.error instanceof Error
? localAliasesState.error.message
: 'Unkonw Error'}
</Text>
</Box>
)}

View file

@ -16,18 +16,19 @@ import {
Text,
} from 'folds';
import React, { useCallback, useState } from 'react';
import { MatrixError } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { putStateEvent } from 'neurone/lib/api/matrix/client/v3/rooms';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { StateEvent } from '../../../../types/matrix/room';
import { StateEventType } 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';
import { useClient } from '../../../hooks/useClient';
import { useObservableValue } from '../../../hooks/useObservableValue';
const ROOM_ENC_ALGO = 'm.megolm.v1.aes-sha2';
@ -35,21 +36,26 @@ type RoomEncryptionProps = {
permissions: RoomPermissionsAPI;
};
export function RoomEncryption({ permissions }: RoomEncryptionProps) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = useRoom();
const canEnable = permissions.stateEvent(StateEvent.RoomEncryption, mx.getSafeUserId());
const content = useStateEvent(room, StateEvent.RoomEncryption)?.getContent<{
algorithm: string;
}>();
const enabled = content?.algorithm === ROOM_ENC_ALGO;
const canEnable = permissions.stateEvent(StateEventType.RoomEncryption, ct.session.userId);
const encryptedRooms = useObservableValue(ct.encryptedRooms);
const enabled = encryptedRooms.has(roomId);
const [enableState, enable] = useAsyncCallback(
useCallback(async () => {
await mx.sendStateEvent(room.roomId, StateEvent.RoomEncryption as any, {
algorithm: ROOM_ENC_ALGO,
});
}, [mx, room.roomId])
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomEncryption, stateKey: '' },
body: {
algorithm: ROOM_ENC_ALGO,
},
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
}, [ct, roomId])
);
const enabling = enableState.status === AsyncStatus.Loading;
@ -97,7 +103,7 @@ export function RoomEncryption({ permissions }: RoomEncryptionProps) {
>
{enableState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T200">
{(enableState.error as MatrixError).message}
{enableState.error instanceof Error ? enableState.error.message : 'Unknown Error'}
</Text>
)}
{prompt && (

View file

@ -12,55 +12,46 @@ import {
Spinner,
Text,
} from 'folds';
import { HistoryVisibility, MatrixError } from 'matrix-js-sdk';
import { RoomHistoryVisibilityEventContent } from 'matrix-js-sdk/lib/types';
import FocusTrap from 'focus-trap-react';
import { HistoryVisibility } from 'neurone/lib/crypto/machine';
import { putStateEvent } from 'neurone/lib/api/matrix/client/v3/rooms';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRoom } from '../../../hooks/useRoom';
import { StateEvent } from '../../../../types/matrix/room';
import { RoomHistoryVisibilityContent, StateEventType } 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';
import { useClient } from '../../../hooks/useClient';
import { useObservableValue } from '../../../hooks/useObservableValue';
const useVisibilityStr = () =>
const useVisibilityStr = (): Record<HistoryVisibility, string> =>
useMemo(
() => ({
[HistoryVisibility.Invited]: 'After Invite',
[HistoryVisibility.Joined]: 'After Join',
[HistoryVisibility.Shared]: 'All Messages',
[HistoryVisibility.WorldReadable]: 'All Messages (Guests)',
invited: 'After Invite',
joined: 'After Join',
shared: 'All Messages',
world_readable: 'All Messages (Guests)',
}),
[]
);
const useVisibilityMenu = () =>
useMemo(
() => [
HistoryVisibility.Shared,
HistoryVisibility.Invited,
HistoryVisibility.Joined,
HistoryVisibility.WorldReadable,
],
[]
);
const useVisibilityMenu = (): HistoryVisibility[] =>
useMemo(() => ['invited', 'joined', 'shared', 'world_readable'], []);
type RoomHistoryVisibilityProps = {
permissions: RoomPermissionsAPI;
};
export function RoomHistoryVisibility({ permissions }: RoomHistoryVisibilityProps) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = useRoom();
const canEdit = permissions.stateEvent(StateEvent.RoomHistoryVisibility, mx.getSafeUserId());
const canEdit = permissions.stateEvent(StateEventType.RoomHistoryVisibility, ct.session.userId);
const roomToHistoryVisibility = useObservableValue(ct.roomToHistoryVisibility);
const visibilityEvent = useStateEvent(room, StateEvent.RoomHistoryVisibility);
const historyVisibility: HistoryVisibility =
visibilityEvent?.getContent<RoomHistoryVisibilityEventContent>().history_visibility ??
HistoryVisibility.Shared;
const historyVisibility: HistoryVisibility = roomToHistoryVisibility.get(roomId) ?? 'shared';
const visibilityMenu = useVisibilityMenu();
const visibilityStr = useVisibilityStr();
@ -73,12 +64,20 @@ export function RoomHistoryVisibility({ permissions }: RoomHistoryVisibilityProp
const [submitState, submit] = useAsyncCallback(
useCallback(
async (visibility: HistoryVisibility) => {
const content: RoomHistoryVisibilityEventContent = {
const content: RoomHistoryVisibilityContent = {
history_visibility: visibility,
};
await mx.sendStateEvent(room.roomId, StateEvent.RoomHistoryVisibility as any, content);
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomHistoryVisibility, stateKey: '' },
body: content,
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
},
[mx, room.roomId]
[ct, roomId]
)
);
const submitting = submitState.status === AsyncStatus.Loading;
@ -156,7 +155,7 @@ export function RoomHistoryVisibility({ permissions }: RoomHistoryVisibilityProp
>
{submitState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T200">
{(submitState.error as MatrixError).message}
{submitState.error instanceof Error ? submitState.error.message : 'Unknown Error'}
</Text>
)}
</SettingTile>

View file

@ -1,10 +1,9 @@
import React, { useCallback, useMemo } from 'react';
import { color, Text } from 'folds';
import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { useAtomValue } from 'jotai';
import { putStateEvent } from 'neurone/lib/api/matrix/client/v3/rooms';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import {
ExtendedJoinRules,
JoinRulesSwitcher,
useJoinRuleIcons,
useRoomJoinRuleLabel,
@ -12,13 +11,15 @@ import {
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRoom } from '../../../hooks/useRoom';
import { StateEvent } from '../../../../types/matrix/room';
import { useStateEvent } from '../../../hooks/useStateEvent';
import {
JoinRule,
StateEventType,
AllowCondition,
JoinRulesContent,
} from '../../../../types/matrix/room';
import { useSpaceOptionally } from '../../../hooks/useSpace';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getStateEvents } from '../../../utils/room';
import {
useRecursiveChildSpaceScopeFactory,
useSpaceChildren,
@ -31,89 +32,98 @@ import {
restrictedSupported,
} from '../../../utils/matrix';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
type RestrictedRoomAllowContent = {
room_id: string;
type: RestrictedAllowType;
};
import { useClient } from '../../../hooks/useClient';
import { useObservableValue } from '../../../hooks/useObservableValue';
import { getRoomStateEventsQuery } from '../../../hooks/storage/useRoomStateEvent';
type RoomJoinRulesProps = {
permissions: RoomPermissionsAPI;
};
export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
const mx = useMatrixClient();
const room = useRoom();
const allowKnockRestricted = knockRestrictedSupported(room.getVersion());
const allowRestricted = restrictedSupported(room.getVersion());
const allowKnock = knockSupported(room.getVersion());
const ct = useClient();
const roomId = useRoom();
const roomVersion = ct.roomToVersion.read.get(roomId) ?? '1';
const allowKnockRestricted = knockRestrictedSupported(roomVersion);
const allowRestricted = restrictedSupported(roomVersion);
const allowKnock = knockSupported(roomVersion);
const roomIdToParents = useAtomValue(roomToParentsAtom);
const space = useSpaceOptionally();
const subspacesScope = useRecursiveChildSpaceScopeFactory(mx, roomIdToParents);
const subspaces = useSpaceChildren(allRoomsAtom, space?.roomId ?? '', subspacesScope);
const spaceId = useSpaceOptionally();
const subspacesScope = useRecursiveChildSpaceScopeFactory(ct, roomIdToParents);
const subspaces = useSpaceChildren(allRoomsAtom, spaceId ?? '', subspacesScope);
const canEdit = permissions.stateEvent(StateEvent.RoomHistoryVisibility, mx.getSafeUserId());
const canEdit = permissions.stateEvent(StateEventType.RoomHistoryVisibility, ct.session.userId);
const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules);
const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>();
const rule: JoinRule = content?.join_rule ?? JoinRule.Invite;
const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
const rule: JoinRule = roomToJoinRule.get(roomId) ?? 'invite';
const joinRules: Array<ExtendedJoinRules> = useMemo(() => {
const r: ExtendedJoinRules[] = [JoinRule.Invite];
const joinRules: Array<JoinRule> = useMemo(() => {
const r: JoinRule[] = ['invite'];
if (allowKnock) {
r.push(JoinRule.Knock);
r.push('knock');
}
if (allowRestricted && space) {
r.push(JoinRule.Restricted);
if (allowRestricted && spaceId) {
r.push('restricted');
}
if (allowKnockRestricted && space) {
if (allowKnockRestricted && spaceId) {
r.push('knock_restricted');
}
r.push(JoinRule.Public);
r.push('public');
return r;
}, [allowKnockRestricted, allowRestricted, allowKnock, space]);
}, [allowKnockRestricted, allowRestricted, allowKnock, spaceId]);
const icons = useJoinRuleIcons(room.getType());
const icons = useJoinRuleIcons(ct.roomToType.read.get(roomId));
const labels = useRoomJoinRuleLabel();
const [submitState, submit] = useAsyncCallback(
useCallback(
async (joinRule: ExtendedJoinRules) => {
const allow: RestrictedRoomAllowContent[] = [];
if (joinRule === JoinRule.Restricted || joinRule === 'knock_restricted') {
const roomParents = roomIdToParents.get(room.roomId);
async (joinRule: JoinRule) => {
const allow: AllowCondition[] = [];
if (joinRule === 'restricted' || joinRule === 'knock_restricted') {
const roomParents = roomIdToParents.get(roomId);
const parents = getStateEvents(room, StateEvent.SpaceParent)
.map((event) => event.getStateKey())
.filter((parentId) => typeof parentId === 'string')
const parentEvents = await ct.storage.joinRoomState.read.query(
getRoomStateEventsQuery(roomId, StateEventType.SpaceParent)
);
const parents = parentEvents
.map((event) => event.event.state_key)
.filter((parentId) => roomParents?.has(parentId));
if (parents.length === 0 && space && roomParents) {
if (parents.length === 0 && spaceId && roomParents) {
// if no m.space.parent found
// find parent in current space
const selectedParents = subspaces.filter((rId) => roomParents.has(rId));
if (roomParents.has(space.roomId)) {
selectedParents.push(space.roomId);
if (roomParents.has(spaceId)) {
selectedParents.push(spaceId);
}
selectedParents.forEach((pId) => parents.push(pId));
}
parents.forEach((parentRoomId) => {
if (!parentRoomId) return;
allow.push({
type: RestrictedAllowType.RoomMembership,
type: 'm.room_membership',
room_id: parentRoomId,
});
});
}
const c: RoomJoinRulesEventContent = {
join_rule: joinRule as JoinRule,
const c: JoinRulesContent = {
join_rule: joinRule,
};
if (allow.length > 0) c.allow = allow;
await mx.sendStateEvent(room.roomId, StateEvent.RoomJoinRules as any, c);
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomJoinRules, stateKey: '' },
body: c,
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
},
[mx, room, space, subspaces, roomIdToParents]
[ct, roomId, spaceId, subspaces, roomIdToParents]
)
);
@ -127,9 +137,9 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
gap="400"
>
<SettingTile
title={room.isSpaceRoom() ? 'Space Access' : 'Room Access'}
title={ct.spaceRooms.read.has(roomId) ? 'Space Access' : 'Room Access'}
description={
room.isSpaceRoom()
ct.spaceRooms.read.has(roomId)
? 'Change how people can join the space.'
: 'Change how people can join the room.'
}
@ -147,7 +157,7 @@ export function RoomJoinRules({ permissions }: RoomJoinRulesProps) {
>
{submitState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T200">
{(submitState.error as MatrixError).message}
{submitState.error instanceof Error ? submitState.error.message : 'Unknown Error'}
</Text>
)}
</SettingTile>

View file

@ -12,27 +12,18 @@ import {
TextArea,
} from 'folds';
import React, { FormEventHandler, useCallback, useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import Linkify from 'linkify-react';
import classNames from 'classnames';
import { JoinRule, MatrixError } from 'matrix-js-sdk';
import { putStateEvent } from 'neurone/lib/api/matrix/client/v3/rooms';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../../room-settings/styles.css';
import { useRoom } from '../../../hooks/useRoom';
import {
useRoomAvatar,
useRoomJoinRule,
useRoomName,
useRoomTopic,
} from '../../../hooks/useRoomMeta';
import { mDirectAtom } from '../../../state/mDirectList';
import { BreakWord, LineClamp3 } from '../../../styles/Text.css';
import { LINKIFY_OPTS } from '../../../plugins/react-custom-html-parser';
import { RoomAvatar, RoomIcon } from '../../../components/room-avatar';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { StateEvent } from '../../../../types/matrix/room';
import { JoinRule, StateEvent, StateEventType } from '../../../../types/matrix/room';
import { CompactUploadCardRenderer } from '../../../components/upload-card';
import { useObjectURL } from '../../../hooks/useObjectURL';
import { createUploadAtom, UploadSuccess } from '../../../state/upload';
@ -40,6 +31,9 @@ import { useFilePicker } from '../../../hooks/useFilePicker';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useAlive } from '../../../hooks/useAlive';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
import { useClient } from '../../../hooks/useClient';
import { useObservableValue } from '../../../hooks/useObservableValue';
import { mxcToHttp } from '../../../../client/mxcToHttp';
type RoomProfileEditProps = {
canEditAvatar: boolean;
@ -48,6 +42,7 @@ type RoomProfileEditProps = {
avatar?: string;
name: string;
topic: string;
joinRule?: JoinRule;
onClose: () => void;
};
export function RoomProfileEdit({
@ -57,17 +52,22 @@ export function RoomProfileEdit({
avatar,
name,
topic,
joinRule,
onClose,
}: RoomProfileEditProps) {
const room = useRoom();
const mx = useMatrixClient();
const alive = useAlive();
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const joinRule = useRoomJoinRule(room);
const roomId = useRoom();
const alive = useAlive();
const [roomAvatar, setRoomAvatar] = useState(avatar);
const avatarUrl = roomAvatar
? mxcUrlToHttp(mx, roomAvatar, useAuthentication) ?? undefined
? mxcToHttp(ct.session.baseUrl, roomAvatar, useAuthentication).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined;
const [imageFile, setImageFile] = useState<File>();
@ -93,18 +93,37 @@ export function RoomProfileEdit({
useCallback(
async (roomAvatarMxc?: string | null, roomName?: string, roomTopic?: string) => {
if (roomAvatarMxc !== undefined) {
await mx.sendStateEvent(room.roomId, StateEvent.RoomAvatar as any, {
url: roomAvatarMxc,
});
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomAvatar, stateKey: '' },
body: { url: roomAvatarMxc },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
}
if (roomName !== undefined) {
await mx.sendStateEvent(room.roomId, StateEvent.RoomName as any, { name: roomName });
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomName, stateKey: '' },
body: { name: roomName },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
}
if (roomTopic !== undefined) {
await mx.sendStateEvent(room.roomId, StateEvent.RoomTopic as any, { topic: roomTopic });
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomTopic, stateKey: '' },
body: { topic: roomTopic },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
}
},
[mx, room.roomId]
[ct, roomId]
)
);
const submitting = submitState.status === AsyncStatus.Loading;
@ -194,14 +213,14 @@ export function RoomProfileEdit({
<Box shrink="No">
<Avatar size="500" radii="300">
<RoomAvatar
roomId={room.roomId}
roomId={roomId}
src={avatarUrl}
alt={name}
renderFallback={() => (
<RoomIcon
roomType={room.getType()}
roomType={ct.roomToType.read.get(roomId)}
size="400"
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
joinRule={joinRule}
filled
/>
)}
@ -231,7 +250,7 @@ export function RoomProfileEdit({
</Box>
{submitState.status === AsyncStatus.Error && (
<Text size="T200" style={{ color: color.Critical.Main }}>
{(submitState.error as MatrixError).message}
{submitState.error instanceof Error ? submitState.error.message : 'Unknown Error'}
</Text>
)}
<Box gap="300">
@ -264,23 +283,32 @@ type RoomProfileProps = {
permissions: RoomPermissionsAPI;
};
export function RoomProfile({ permissions }: RoomProfileProps) {
const mx = useMatrixClient();
const ct = useClient();
const myUserId = ct.session.userId;
const useAuthentication = useMediaAuthentication();
const room = useRoom();
const directs = useAtomValue(mDirectAtom);
const roomId = useRoom();
const avatar = useRoomAvatar(room, directs.has(room.roomId));
const name = useRoomName(room);
const topic = useRoomTopic(room);
const joinRule = useRoomJoinRule(room);
const roomToAvatar = useObservableValue(ct.roomToAvatar);
const roomToName = useObservableValue(ct.roomToName);
const roomToTopic = useObservableValue(ct.roomToTopic);
const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
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 avatar = roomToAvatar.get(roomId);
const name = roomToName.get(roomId);
const topic = roomToTopic.get(roomId);
const joinRule = roomToJoinRule.get(roomId);
const canEditAvatar = permissions.stateEvent(StateEvent.RoomAvatar, myUserId);
const canEditName = permissions.stateEvent(StateEvent.RoomName, myUserId);
const canEditTopic = permissions.stateEvent(StateEvent.RoomTopic, myUserId);
const canEdit = canEditAvatar || canEditName || canEditTopic;
const avatarUrl = avatar
? mxcUrlToHttp(mx, avatar, useAuthentication, 96, 96, 'crop') ?? undefined
? mxcToHttp(ct.session.baseUrl, avatar, useAuthentication).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined;
const [edit, setEdit] = useState(false);
@ -304,6 +332,7 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
avatar={avatar}
name={name ?? ''}
topic={topic ?? ''}
joinRule={joinRule}
onClose={handleCloseEdit}
/>
) : (
@ -337,14 +366,14 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
<Box shrink="No">
<Avatar size="500" radii="300">
<RoomAvatar
roomId={room.roomId}
roomId={roomId}
src={avatarUrl}
alt={name}
renderFallback={() => (
<RoomIcon
roomType={room.getType()}
roomType={ct.roomToType.read.get(roomId)}
size="400"
joinRule={joinRule?.join_rule ?? JoinRule.Invite}
joinRule={joinRule}
filled
/>
)}

View file

@ -1,42 +1,37 @@
import React from 'react';
import { Box, color, Spinner, Switch, Text } from 'folds';
import { JoinRule, MatrixError } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } 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 { useRoomDirectoryVisibility } from '../../../hooks/useRoomDirectoryVisibility';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { StateEvent } from '../../../../types/matrix/room';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useStateEvent } from '../../../hooks/useStateEvent';
import { ExtendedJoinRules } from '../../../components/JoinRulesSwitcher';
import { JoinRule, StateEventType } from '../../../../types/matrix/room';
import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions';
import { useClient } from '../../../hooks/useClient';
import { useObservableValue } from '../../../hooks/useObservableValue';
type RoomPublishProps = {
permissions: RoomPermissionsAPI;
};
export function RoomPublish({ permissions }: RoomPublishProps) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = useRoom();
const canEditCanonical = permissions.stateEvent(
StateEvent.RoomCanonicalAlias,
mx.getSafeUserId()
StateEventType.RoomCanonicalAlias,
ct.session.userId
);
const joinRuleEvent = useStateEvent(room, StateEvent.RoomJoinRules);
const content = joinRuleEvent?.getContent<RoomJoinRulesEventContent>();
const rule: ExtendedJoinRules = (content?.join_rule as ExtendedJoinRules) ?? JoinRule.Invite;
const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
const rule: JoinRule = roomToJoinRule.get(roomId) ?? 'invite';
const { visibilityState, setVisibility } = useRoomDirectoryVisibility(room.roomId);
const { visibilityState, setVisibility } = useRoomDirectoryVisibility(roomId);
const [toggleState, toggleVisibility] = useAsyncCallback(setVisibility);
const loading =
visibilityState.status === AsyncStatus.Loading || toggleState.status === AsyncStatus.Loading;
const validRule =
rule === JoinRule.Public || rule === JoinRule.Knock || rule === 'knock_restricted';
const validRule = rule === 'public' || rule === 'knock' || rule === 'knock_restricted';
return (
<SequenceCard
@ -48,7 +43,7 @@ export function RoomPublish({ permissions }: RoomPublishProps) {
<SettingTile
title="Publish to Directory"
description={
room.isSpaceRoom()
ct.spaceRooms.read.has(roomId)
? 'List the space in the public directory to make it discoverable by others.'
: 'List the room in the public directory to make it discoverable by others.'
}
@ -67,13 +62,15 @@ export function RoomPublish({ permissions }: RoomPublishProps) {
>
{visibilityState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T200">
{(visibilityState.error as MatrixError).message}
{visibilityState.error instanceof Error
? visibilityState.error.message
: 'Unknown Error'}
</Text>
)}
{toggleState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T200">
{(toggleState.error as MatrixError).message}
{toggleState.error instanceof Error ? toggleState.error.message : 'Unknown Error'}
</Text>
)}
</SettingTile>

View file

@ -16,16 +16,15 @@ import {
Icons,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { MatrixError, Method } from 'matrix-js-sdk';
import { RoomTombstoneEventContent } from 'matrix-js-sdk/lib/types';
import { ObjectReader } from 'neurone/lib/utils';
import { postUpgrade } from 'neurone/lib/api/matrix/client/v3/rooms';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
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 { IRoomCreateContent, StateEvent } from '../../../../types/matrix/room';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useStateEvent } from '../../../hooks/useStateEvent';
import { StateEventType } from '../../../../types/matrix/room';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
import { useCapabilities } from '../../../hooks/useCapabilities';
import { stopPropagation } from '../../../utils/keyboard';
@ -39,12 +38,14 @@ import { useAlive } from '../../../hooks/useAlive';
import { creatorsSupported } from '../../../utils/matrix';
import { useRoomCreators } from '../../../hooks/useRoomCreators';
import { BreakWord } from '../../../styles/Text.css';
import { useClient } from '../../../hooks/useClient';
const EMPTY_CREATORS = new Set<string>();
function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = useRoom();
const alive = useAlive();
const creators = useRoomCreators(room);
const creators = useRoomCreators(roomId) ?? EMPTY_CREATORS;
const capabilities = useCapabilities();
const roomVersions = capabilities['m.room_versions'];
@ -61,12 +62,20 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
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,
});
const resp = await ct.requestAuthJSON(
postUpgrade({
pathParams: { roomId },
body: {
new_version: version,
additional_creators: newAdditionalCreators,
},
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
},
[mx, room]
[ct, roomId]
)
);
@ -103,7 +112,9 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
size="500"
>
<Box grow="Yes">
<Text size="H4">{room.isSpaceRoom() ? 'Space Upgrade' : 'Room Upgrade'}</Text>
<Text size="H4">
{ct.spaceRooms.read.has(roomId) ? 'Space Upgrade' : 'Room Upgrade'}
</Text>
</Box>
<IconButton size="300" onClick={requestClose} radii="300">
<Icon src={Icons.Cross} />
@ -139,7 +150,9 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
</Box>
{upgradeState.status === AsyncStatus.Error && (
<Text className={BreakWord} style={{ color: color.Critical.Main }} size="T200">
{(upgradeState.error as MatrixError).message}
{upgradeState.error instanceof Error
? upgradeState.error.message
: 'Unknown Error'}
</Text>
)}
<Button
@ -148,7 +161,9 @@ function RoomUpgradeDialog({ requestClose }: { requestClose: () => void }) {
disabled={upgrading}
before={upgrading && <Spinner size="200" variant="Secondary" fill="Solid" />}
>
<Text size="B400">{room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}</Text>
<Text size="B400">
{ct.spaceRooms.read.has(roomId) ? 'Upgrade Space' : 'Upgrade Room'}
</Text>
</Button>
</Box>
</Dialog>
@ -163,28 +178,30 @@ type RoomUpgradeProps = {
requestClose: () => void;
};
export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
const mx = useMatrixClient();
const room = useRoom();
const ct = useClient();
const roomId = useRoom();
const { navigateRoom, navigateSpace } = useRoomNavigate();
const createContent = useStateEvent(
room,
StateEvent.RoomCreate
)?.getContent<IRoomCreateContent>();
const roomVersion = createContent?.room_version ?? '1';
const predecessorRoomId = createContent?.predecessor?.room_id;
const tombstoneContent = useStateEvent(
room,
StateEvent.RoomTombstone
)?.getContent<RoomTombstoneEventContent>();
const replacementRoom = tombstoneContent?.replacement_room;
const createContent = ct.state.join.create.read.find((event) => event.roomId === roomId)?.event
.content;
const tombstoneContent = ct.state.join.tombstone.read.find((event) => event.roomId === roomId)
?.event.content;
const canUpgrade = permissions.stateEvent(StateEvent.RoomTombstone, mx.getSafeUserId());
const roomVersion = ObjectReader.string(createContent, 'room_version') ?? '1';
const predecessorRoomId = ObjectReader.string(
ObjectReader.object(createContent, 'predecessor'),
'room_id'
);
const replacementRoom = ObjectReader.string(tombstoneContent, 'replacement_room');
const tombstoneBody = ObjectReader.string(tombstoneContent, 'body');
const canUpgrade = permissions.stateEvent(StateEventType.RoomTombstone, ct.session.userId);
const handleOpenRoom = () => {
if (replacementRoom) {
requestClose();
if (room.isSpaceRoom()) {
if (ct.spaceRooms.read.has(roomId)) {
navigateSpace(replacementRoom);
} else {
navigateRoom(replacementRoom);
@ -195,10 +212,14 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
const handleOpenOldRoom = () => {
if (predecessorRoomId) {
requestClose();
if (room.isSpaceRoom()) {
if (ct.spaceRooms.read.has(roomId)) {
navigateSpace(predecessorRoomId);
} else {
navigateRoom(predecessorRoomId, createContent.predecessor?.event_id);
const preEventId = ObjectReader.string(
ObjectReader.object(createContent, 'predecessor'),
'event_id'
);
navigateRoom(predecessorRoomId, preEventId);
}
}
};
@ -213,11 +234,11 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
gap="400"
>
<SettingTile
title={room.isSpaceRoom() ? 'Upgrade Space' : 'Upgrade Room'}
title={ct.spaceRooms.read.has(roomId) ? 'Upgrade Space' : 'Upgrade Room'}
description={
replacementRoom
? tombstoneContent.body ||
`This ${room.isSpaceRoom() ? 'space' : 'room'} has been replaced!`
? tombstoneBody ||
`This ${ct.spaceRooms.read.has(roomId) ? 'space' : 'room'} has been replaced!`
: `Current version: ${roomVersion}.`
}
after={
@ -231,7 +252,7 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
radii="300"
onClick={handleOpenOldRoom}
>
<Text size="B300">{room.isSpaceRoom() ? 'Old Space' : 'Old Room'}</Text>
<Text size="B300">{ct.spaceRooms.read.has(roomId) ? 'Old Space' : 'Old Room'}</Text>
</Button>
)}
{replacementRoom ? (
@ -242,7 +263,9 @@ export function RoomUpgrade({ permissions, requestClose }: RoomUpgradeProps) {
radii="300"
onClick={handleOpenRoom}
>
<Text size="B300">{room.isSpaceRoom() ? 'Open New Space' : 'Open New Room'}</Text>
<Text size="B300">
{ct.spaceRooms.read.has(roomId) ? 'Open New Space' : 'Open New Room'}
</Text>
</Button>
) : (
<Button

View file

@ -1,14 +1,8 @@
import React, { useMemo, useState } from 'react';
import { useAtomValue } from 'jotai';
import { Avatar, Box, config, Icon, IconButton, Icons, IconSrc, MenuItem, Text } from 'folds';
import { JoinRule } from 'matrix-js-sdk';
import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../components/page';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useRoomAvatar, useRoomJoinRule, useRoomName } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { General } from './general';
import { Members } from '../common-settings/members';
@ -17,6 +11,22 @@ import { Permissions } from './permissions';
import { RoomSettingsPage } from '../../state/roomSettings';
import { useRoom } from '../../hooks/useRoom';
import { DeveloperTools } from '../common-settings/developer-tools';
import { useClient } from '../../hooks/useClient';
import { useRoomHeroes } from '../../hooks/storage/useRoomHeros';
import { useGetRoomName } from '../../hooks/storage/useGetRoomName';
import { useGetRoomAvatar } from '../../hooks/storage/useGetRoomAvatar';
import { mxcToHttp } from '../../../client/mxcToHttp';
import { useObservableValue } from '../../hooks/useObservableValue';
import {
PowerLevelsContextProvider,
usePowerLevels,
usePowerLevelsState,
} from '../../hooks/usePowerLevels';
import {
PowerLevelTagsContextProvider,
usePowerLevelTags,
usePowerLevelTagsState,
} from '../../hooks/usePowerLevelTags';
type RoomSettingsMenuItem = {
page: RoomSettingsPage;
@ -61,17 +71,29 @@ type RoomSettingsProps = {
requestClose: () => void;
};
export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
const room = useRoom();
const mx = useMatrixClient();
const roomId = useRoom();
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const mDirects = useAtomValue(mDirectAtom);
const roomAvatar = useRoomAvatar(room, mDirects.has(room.roomId));
const roomName = useRoomName(room);
const joinRuleContent = useRoomJoinRule(room);
const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(roomId);
const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
const roomAvatar = getRoomAvatar(roomId);
const roomName = getRoomName(roomId);
const stateObjResult = usePowerLevelsState(roomId);
const powerLevels = usePowerLevels(stateObjResult.value);
const powerLevelTagsResult = usePowerLevelTagsState(roomId);
const powerLevelTags = usePowerLevelTags(powerLevels, powerLevelTagsResult.value);
const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
const avatarUrl = roomAvatar
? mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined
? mxcToHttp(ct.session.baseUrl, roomAvatar, useAuthentication).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined;
const screenSize = useScreenSizeContext();
@ -90,84 +112,91 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
};
return (
<PageRoot
nav={
screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
<PageNav size="300">
<PageNavHeader outlined={false}>
<Box grow="Yes" gap="200">
<Avatar size="200" radii="300">
<RoomAvatar
roomId={room.roomId}
src={avatarUrl}
alt={roomName}
renderFallback={() => (
<RoomIcon
size="50"
roomType={room.getType()}
joinRule={joinRuleContent?.join_rule ?? JoinRule.Invite}
filled
<PowerLevelsContextProvider value={powerLevels}>
<PowerLevelTagsContextProvider value={powerLevelTags}>
<PageRoot
nav={
screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : (
<PageNav size="300">
<PageNavHeader outlined={false}>
<Box grow="Yes" gap="200">
<Avatar size="200" radii="300">
<RoomAvatar
roomId={roomId}
src={avatarUrl}
alt={roomName}
renderFallback={() => (
<RoomIcon
size="50"
roomType={ct.roomToType.read.get(roomId)}
joinRule={roomToJoinRule.get(roomId)}
filled
/>
)}
/>
</Avatar>
<Text size="H4" truncate>
{roomName}
</Text>
</Box>
<Box shrink="No">
{screenSize === ScreenSize.Mobile && (
<IconButton onClick={requestClose} variant="Background">
<Icon src={Icons.Cross} />
</IconButton>
)}
/>
</Avatar>
<Text size="H4" truncate>
{roomName}
</Text>
</Box>
<Box shrink="No">
{screenSize === ScreenSize.Mobile && (
<IconButton onClick={requestClose} variant="Background">
<Icon src={Icons.Cross} />
</IconButton>
)}
</Box>
</PageNavHeader>
<Box grow="Yes" direction="Column">
<PageNavContent>
<div style={{ flexGrow: 1 }}>
{menuItems.map((item) => (
<MenuItem
key={item.name}
variant="Background"
radii="400"
aria-pressed={activePage === item.page}
before={<Icon src={item.icon} size="100" filled={activePage === item.page} />}
onClick={() => setActivePage(item.page)}
>
<Text
style={{
fontWeight: activePage === item.page ? config.fontWeight.W600 : undefined,
}}
size="T300"
truncate
>
{item.name}
</Text>
</MenuItem>
))}
</div>
</PageNavContent>
</Box>
</PageNav>
)
}
>
{activePage === RoomSettingsPage.GeneralPage && (
<General requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.MembersPage && (
<Members requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.PermissionsPage && (
<Permissions requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.EmojisStickersPage && (
<EmojisStickers requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.DeveloperToolsPage && (
<DeveloperTools requestClose={handlePageRequestClose} />
)}
</PageRoot>
</Box>
</PageNavHeader>
<Box grow="Yes" direction="Column">
<PageNavContent>
<div style={{ flexGrow: 1 }}>
{menuItems.map((item) => (
<MenuItem
key={item.name}
variant="Background"
radii="400"
aria-pressed={activePage === item.page}
before={
<Icon src={item.icon} size="100" filled={activePage === item.page} />
}
onClick={() => setActivePage(item.page)}
>
<Text
style={{
fontWeight:
activePage === item.page ? config.fontWeight.W600 : undefined,
}}
size="T300"
truncate
>
{item.name}
</Text>
</MenuItem>
))}
</div>
</PageNavContent>
</Box>
</PageNav>
)
}
>
{activePage === RoomSettingsPage.GeneralPage && (
<General requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.MembersPage && (
<Members requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.PermissionsPage && (
<Permissions requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.EmojisStickersPage && (
<EmojisStickers requestClose={handlePageRequestClose} />
)}
{activePage === RoomSettingsPage.DeveloperToolsPage && (
<DeveloperTools requestClose={handlePageRequestClose} />
)}
</PageRoot>
</PowerLevelTagsContextProvider>
</PowerLevelsContextProvider>
);
}

View file

@ -2,10 +2,11 @@ import React from 'react';
import { RoomSettings } from './RoomSettings';
import { Modal500 } from '../../components/Modal500';
import { useCloseRoomSettings, useRoomSettingsState } from '../../state/hooks/roomSettings';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { RoomSettingsState } from '../../state/roomSettings';
import { RoomProvider } from '../../hooks/useRoom';
import { SpaceProvider } from '../../hooks/useSpace';
import { useClient } from '../../hooks/useClient';
import { useObservableValue } from '../../hooks/useObservableValue';
type RenderSettingsProps = {
state: RoomSettingsState;
@ -13,17 +14,15 @@ type RenderSettingsProps = {
function RenderSettings({ state }: RenderSettingsProps) {
const { roomId, spaceId, page } = state;
const closeSettings = useCloseRoomSettings();
const allJoinedRooms = useAllJoinedRoomsSet();
const getRoom = useGetRoom(allJoinedRooms);
const room = getRoom(roomId);
const space = spaceId ? getRoom(spaceId) : undefined;
const ct = useClient();
const rooms = useObservableValue(ct.rooms.join);
if (!room) return null;
if (!rooms.includes(roomId)) return null;
return (
<Modal500 requestClose={closeSettings}>
<SpaceProvider value={space ?? null}>
<RoomProvider value={room}>
<SpaceProvider value={spaceId ?? null}>
<RoomProvider value={roomId}>
<RoomSettings initialPage={page} requestClose={closeSettings} />
</RoomProvider>
</SpaceProvider>

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { usePowerLevels } from '../../../hooks/usePowerLevels';
import { usePowerLevelsContext } from '../../../hooks/usePowerLevels';
import { useRoom } from '../../../hooks/useRoom';
import {
RoomProfile,
@ -21,8 +21,8 @@ type GeneralProps = {
};
export function General({ requestClose }: GeneralProps) {
const room = useRoom();
const powerLevels = usePowerLevels(room);
const creators = useRoomCreators(room);
const powerLevels = usePowerLevelsContext();
const permissions = useRoomPermissions(creators, powerLevels);
return (

View file

@ -23,19 +23,19 @@ export const useRoomStateEvent = (
return stateEventResult;
};
export const getRoomStateEventsQuery = (roomId: string, eventType: string) =>
new QueryRange(
getJoinRoomStateId(roomId, eventType, ''),
getJoinRoomStateId(roomId, eventType, MAX_UNICODE)
);
export const useRoomStateEvents = (
roomId: string,
eventType: string
): PendingObservableResult<JoinRoomStateObject[]> => {
const ct = useClient();
const stateEventPromise = useMemo(
() =>
ct.storage.joinRoomState.observableRead.query(
new QueryRange(
getJoinRoomStateId(roomId, eventType, ''),
getJoinRoomStateId(roomId, eventType, MAX_UNICODE)
)
),
() => ct.storage.joinRoomState.observableRead.query(getRoomStateEventsQuery(roomId, eventType)),
[ct, roomId, eventType]
);

View file

@ -1,83 +1,75 @@
import { useCallback, useEffect, useMemo } from 'react';
import { MatrixError, Room } from 'matrix-js-sdk';
import { RoomCanonicalAliasEventContent } from 'matrix-js-sdk/lib/types';
import { ObjectReader } from 'neurone/lib/utils';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { getAliases, putStateEvent, GetAliasesOK } from 'neurone/lib/api/matrix/client/v3/rooms';
import { deleteRoom, putRoom } from 'neurone/lib/api/matrix/client/v3/directory';
import { AsyncState, useAsyncCallback } from './useAsyncCallback';
import { useMatrixClient } from './useMatrixClient';
import { useAlive } from './useAlive';
import { useStateEvent } from './useStateEvent';
import { StateEvent } from '../../types/matrix/room';
import { getStateEvent } from '../utils/room';
import { useClient } from './useClient';
import { useObservableValue } from './useObservableValue';
import { RoomCanonicalAliasContent, StateEventType } from '../../types/matrix/room';
export const usePublishedAliases = (room: Room): [string | undefined, string[]] => {
const aliasContent = useStateEvent(
room,
StateEvent.RoomCanonicalAlias
)?.getContent<RoomCanonicalAliasEventContent>();
export const usePublishedAliases = (roomId: string): [string | undefined, string[]] => {
const ct = useClient();
const canonicalAliasEvents = useObservableValue(ct.state.join.canonicalAlias);
const content = canonicalAliasEvents.find((state) => state.roomId === roomId)?.event.content;
const canonicalAlias = aliasContent?.alias;
const alias = ObjectReader.string(content, 'alias');
const altAliases = ObjectReader.array(content, 'alt_aliases')?.filter(
(a): a is string => typeof a === 'string'
);
const publishedAliases = useMemo(() => {
const aliases: string[] = [];
if (typeof aliasContent?.alias === 'string') {
aliases.push(aliasContent.alias);
}
aliasContent?.alt_aliases?.forEach((alias) => {
if (typeof alias === 'string') {
aliases.push(alias);
}
});
if (alias) aliases.push(alias);
if (altAliases) aliases.push(...altAliases);
return aliases;
}, [aliasContent]);
}, [alias, altAliases]);
return [canonicalAlias, publishedAliases];
return [alias, publishedAliases];
};
export const useSetMainAlias = (room: Room): ((alias: string | undefined) => Promise<void>) => {
const mx = useMatrixClient();
export const useSetMainAlias = (
roomId: string,
publishedAliases: string[]
): ((alias: string | undefined) => Promise<void>) => {
const ct = useClient();
const mainAlias = useCallback(
async (alias: string | undefined) => {
const content = getStateEvent(
room,
StateEvent.RoomCanonicalAlias
)?.getContent<RoomCanonicalAliasEventContent>();
const altAliases: string[] = [];
if (content?.alias && content.alias !== alias) {
altAliases.push(content.alias);
}
content?.alt_aliases?.forEach((a) => {
if (a !== alias) {
altAliases.push(a);
}
});
const newContent: RoomCanonicalAliasEventContent = {
const content: RoomCanonicalAliasContent = {
alias,
alt_aliases: altAliases,
alt_aliases: publishedAliases.filter((a) => a !== alias),
};
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomCanonicalAlias, stateKey: '' },
body: content,
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
},
[mx, room]
[ct, roomId, publishedAliases]
);
return mainAlias;
};
export const usePublishUnpublishAliases = (
room: Room
roomId: string,
publishedAliases: string[],
canonicalAlias?: string
): {
publishAliases: (aliases: string[]) => Promise<void>;
unpublishAliases: (aliases: string[]) => Promise<void>;
} => {
const mx = useMatrixClient();
const ct = useClient();
const publishAliases = useCallback(
async (aliases: string[]) => {
const content = getStateEvent(
room,
StateEvent.RoomCanonicalAlias
)?.getContent<RoomCanonicalAliasEventContent>();
const altAliases = content?.alt_aliases ?? [];
const altAliases = publishedAliases.filter((a) => a !== canonicalAlias);
aliases.forEach((alias) => {
if (!altAliases.includes(alias)) {
@ -85,38 +77,42 @@ export const usePublishUnpublishAliases = (
}
});
const newContent: RoomCanonicalAliasEventContent = {
alias: content?.alias,
const content: RoomCanonicalAliasContent = {
alias: canonicalAlias,
alt_aliases: altAliases,
};
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomCanonicalAlias, stateKey: '' },
body: content,
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
},
[mx, room]
[ct, roomId, publishedAliases, canonicalAlias]
);
const unpublishAliases = useCallback(
async (aliases: string[]) => {
const content = getStateEvent(
room,
StateEvent.RoomCanonicalAlias
)?.getContent<RoomCanonicalAliasEventContent>();
const altAliases: string[] = [];
const altAliases: string[] = publishedAliases.filter((alias) => !aliases.includes(alias));
content?.alt_aliases?.forEach((alias) => {
if (!aliases.includes(alias)) {
altAliases.push(alias);
}
});
const newContent: RoomCanonicalAliasEventContent = {
alias: content?.alias,
const content: RoomCanonicalAliasContent = {
alias: canonicalAlias,
alt_aliases: altAliases,
};
await mx.sendStateEvent(room.roomId, StateEvent.RoomCanonicalAlias as any, newContent);
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId, eventType: StateEventType.RoomCanonicalAlias, stateKey: '' },
body: content,
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
},
[mx, room]
[ct, roomId, publishedAliases, canonicalAlias]
);
return {
@ -128,18 +124,20 @@ export const usePublishUnpublishAliases = (
export const useLocalAliases = (
roomId: string
): {
localAliasesState: AsyncState<string[], MatrixError>;
localAliasesState: AsyncState<string[], unknown>;
addLocalAlias: (alias: string) => Promise<void>;
removeLocalAlias: (alias: string) => Promise<void>;
} => {
const mx = useMatrixClient();
const ct = useClient();
const alive = useAlive();
const [aliasesState, loadAliases] = useAsyncCallback<string[], MatrixError, []>(
const [aliasesState, loadAliases] = useAsyncCallback<string[], unknown, []>(
useCallback(async () => {
const content = await mx.getLocalAliases(roomId);
return content.aliases;
}, [mx, roomId])
const resp = await ct.requestAuthJSON(getAliases({ pathParams: { roomId } }));
const [err, result] = await parseMatrixResp<GetAliasesOK>(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
return result.aliases;
}, [ct, roomId])
);
useEffect(() => {
@ -148,18 +146,36 @@ export const useLocalAliases = (
const addLocalAlias = useCallback(
async (alias: string) => {
await mx.createAlias(alias, roomId);
const resp = await ct.requestAuthJSON(
putRoom({
pathParams: { roomAlias: alias },
body: { room_id: roomId },
})
);
const [err] = await parseMatrixResp(resp);
if (err) {
if (err.status === 409) {
throw new Error('Address is already in use!');
}
throw new Error(err.data.error ?? err.data.errcode);
}
if (alive()) await loadAliases();
},
[mx, roomId, loadAliases, alive]
[ct, roomId, loadAliases, alive]
);
const removeLocalAlias = useCallback(
async (alias: string) => {
await mx.deleteAlias(alias);
const resp = await ct.requestAuthJSON(deleteRoom({ pathParams: { roomAlias: alias } }));
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
if (alive()) await loadAliases();
},
[mx, loadAliases, alive]
[ct, loadAliases, alive]
);
return {

View file

@ -1,16 +1,23 @@
import {
getListRoom,
GetListRoomOK,
putListRoom,
} from 'neurone/lib/api/matrix/client/v3/directory';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { useCallback, useEffect } from 'react';
import { Visibility } from 'matrix-js-sdk';
import { useAsyncCallback } from './useAsyncCallback';
import { useMatrixClient } from './useMatrixClient';
import { useClient } from './useClient';
export const useRoomDirectoryVisibility = (roomId: string) => {
const mx = useMatrixClient();
const ct = useClient();
const [visibilityState, loadVisibility] = useAsyncCallback(
useCallback(async () => {
const v = await mx.getRoomDirectoryVisibility(roomId);
return v.visibility === Visibility.Public;
}, [mx, roomId])
const resp = await ct.requestAuthJSON(getListRoom({ pathParams: { roomId } }));
const [err, result] = await parseMatrixResp<GetListRoomOK>(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
return result?.visibility === 'public';
}, [ct, roomId])
);
useEffect(() => {
@ -18,14 +25,18 @@ export const useRoomDirectoryVisibility = (roomId: string) => {
}, [loadVisibility]);
const setVisibility = useCallback(
async (visibility: boolean) => {
await mx.setRoomDirectoryVisibility(
roomId,
visibility ? Visibility.Public : Visibility.Private
async (visible: boolean) => {
const resp = await ct.requestAuthJSON(
putListRoom({
pathParams: { roomId },
body: { visibility: visible ? 'public' : 'private' },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(err.data.error ?? err.data.errcode);
await loadVisibility();
},
[mx, roomId, loadVisibility]
[ct, roomId, loadVisibility]
);
return {

View file

@ -139,8 +139,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
<UserRoomProfileRenderer />
<CreateRoomModalRenderer />
<CreateSpaceModalRenderer />
{/*<RoomSettingsRenderer />
<SpaceSettingsRenderer />
<RoomSettingsRenderer />
{/*<SpaceSettingsRenderer />
<ReceiveSelfDeviceVerification />
<AutoRestoreBackupOnVerification /> */}
</ClientRoomsNotificationPreferences>

View file

@ -60,7 +60,7 @@ export const createUploadAtom = (file: UploadContent) => {
file,
status: UploadStatus.Idle,
});
return atom<Upload, [UploadAtomAction], undefined>(
return atom<Upload, [UploadAtomAction], void>(
(get) => get(baseUploadAtom),
(get, set, update) => {
const uploadState = get(baseUploadAtom);

View file

@ -5,6 +5,10 @@ export const BreakWord = style({
wordBreak: 'break-word',
});
export const PreWrap = style({
whiteSpace: 'pre-wrap',
});
export const LineClamp2 = style({
display: '-webkit-box',
WebkitLineClamp: 2,

View file

@ -1,4 +1,5 @@
import { RoomMembership } from 'neurone/lib/api/types';
import { HistoryVisibility } from 'neurone/lib/crypto/machine';
import { IImageInfo } from './common';
export enum Membership {
@ -180,3 +181,11 @@ export type RoomServerAclContent = {
allow_ip_literals?: boolean;
deny?: string[];
};
export type RoomHistoryVisibilityContent = {
history_visibility: HistoryVisibility;
};
export type RoomCanonicalAliasContent = {
alias?: string;
alt_aliases?: string[];
};