replace sdk from space lobby

This commit is contained in:
Ajay Bura 2026-07-20 21:30:29 +05:30
commit 57ca5e4615
20 changed files with 647 additions and 466 deletions

View file

@ -1,4 +1,4 @@
import { ReactNode, useCallback, useState } from 'react';
import { ReactElement, useCallback, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
import { getRoomSummary, GetRoomSummaryOK } from 'neurone/lib/api/matrix/client/v1/roomSummary';
@ -7,10 +7,11 @@ import { SpaceHierarchyRoomChunk } from 'neurone/lib/api/types';
import { AsyncState, AsyncStatus } from '../hooks/useAsyncCallback';
import { useClient } from '../hooks/useClient';
import { useSpecVersionSupport } from '../hooks/useSpecVersions';
import { LocalRoomSummary, useLocalRoomSummary } from '../hooks/useLocalRoomSummary';
type RoomSummaryLoaderProps = {
roomIdOrAlias: string;
children: (roomSummary?: GetRoomSummaryOK) => ReactNode;
children: (roomSummary?: GetRoomSummaryOK) => ReactElement;
};
export function RoomSummaryLoader({ roomIdOrAlias, children }: RoomSummaryLoaderProps) {
@ -40,15 +41,23 @@ export function RoomSummaryLoader({ roomIdOrAlias, children }: RoomSummaryLoader
return children(data);
}
export function LocalRoomSummaryLoader({ children }: { children: () => ReactNode }) {
return children();
export function LocalRoomSummaryLoader({
roomId,
children,
}: {
roomId: string;
children: (summary: LocalRoomSummary) => ReactElement;
}) {
const summary = useLocalRoomSummary(roomId);
return children(summary);
}
export function HierarchyRoomSummaryLoader({
roomId,
children,
}: {
roomId: string;
children: (state: AsyncState<SpaceHierarchyRoomChunk, Error>) => ReactNode;
children: (state: AsyncState<SpaceHierarchyRoomChunk, Error>) => ReactElement;
}) {
const ct = useClient();

View file

@ -186,11 +186,20 @@ export function RoomMentionAutocomplete({
src={avatar}
alt={name}
renderFallback={() => (
<RoomIcon size="50" joinRule={ct.roomToJoinRule.read.get(rId)} filled />
<RoomIcon
size="50"
joinRule={ct.roomToJoinRule.read.get(rId)}
roomType={ct.roomToType.read.get(rId)}
filled
/>
)}
/>
) : (
<RoomIcon size="100" joinRule={ct.roomToJoinRule.read.get(rId)} />
<RoomIcon
size="100"
joinRule={ct.roomToJoinRule.read.get(rId)}
roomType={ct.roomToType.read.get(rId)}
/>
)}
</Avatar>
}

View file

@ -219,7 +219,7 @@ export const RoomCard = as<'div', RoomCardProps>(
)}
/>
</Avatar>
{(roomType === RoomType.Space || localSummary.space) && (
{(roomType === RoomType.Space || localSummary.roomType === RoomType.Space) && (
<Badge variant="Secondary" fill="Soft" outlined>
<Text size="L400">Space</Text>
</Badge>

View file

@ -15,8 +15,8 @@ import {
Spinner,
toRem,
} from 'folds';
import { putStateEvent } from 'neurone/lib/api/matrix/client/v3/rooms';
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { SpaceChildContent, StateEvent } from '../../../types/matrix/room';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { UseStateProvider } from '../../components/UseStateProvider';
@ -27,9 +27,9 @@ 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';
import { InviteUserPrompt } from '../../components/invite-user-prompt';
import { useClient } from '../../hooks/useClient';
type HierarchyItemWithParent = HierarchyItem & {
parentId: string;
@ -42,14 +42,19 @@ function SuggestMenuItem({
item: HierarchyItemWithParent;
requestClose: () => void;
}) {
const mx = useMatrixClient();
const ct = useClient();
const { roomId, parentId, content } = item;
const [toggleState, handleToggleSuggested] = useAsyncCallback(
useCallback(() => {
const newContent: SpaceChildContent = { ...content, suggested: !content.suggested };
return mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, newContent, roomId);
}, [mx, parentId, roomId, content])
return ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId: parentId, eventType: StateEvent.SpaceChild, stateKey: roomId },
body: newContent,
})
);
}, [ct, parentId, roomId, content])
);
useEffect(() => {
@ -80,13 +85,19 @@ function RemoveMenuItem({
item: HierarchyItemWithParent;
requestClose: () => void;
}) {
const mx = useMatrixClient();
const ct = useClient();
const { roomId, parentId } = item;
const [removeState, handleRemove] = useAsyncCallback(
useCallback(
() => mx.sendStateEvent(parentId, StateEvent.SpaceChild as any, {}, roomId),
[mx, parentId, roomId]
() =>
ct.requestAuthJSON(
putStateEvent({
pathParams: { roomId: parentId, eventType: StateEvent.SpaceChild, stateKey: roomId },
body: {},
})
),
[ct, parentId, roomId]
)
);
@ -126,8 +137,6 @@ function InviteMenuItem({
requestClose: () => void;
disabled?: boolean;
}) {
const mx = useMatrixClient();
const room = mx.getRoom(item.roomId);
const [invitePrompt, setInvitePrompt] = useState(false);
const handleInvite = () => {
@ -143,15 +152,15 @@ function InviteMenuItem({
variant="Primary"
fill="None"
aria-pressed={invitePrompt}
disabled={disabled || !room}
disabled={disabled}
>
<Text as="span" size="T300" truncate>
Invite
</Text>
</MenuItem>
{invitePrompt && room && (
{invitePrompt && (
<InviteUserPrompt
room={room}
roomId={item.roomId}
requestClose={() => {
setInvitePrompt(false);
requestClose();
@ -173,13 +182,13 @@ function SettingsMenuItem({
}) {
const openRoomSettings = useOpenRoomSettings();
const openSpaceSettings = useOpenSpaceSettings();
const space = useSpaceOptionally();
const spaceId = useSpaceOptionally();
const handleSettings = () => {
if ('space' in item) {
openSpaceSettings(item.roomId, item.parentId);
} else {
openRoomSettings(item.roomId, space?.roomId);
openRoomSettings(item.roomId, spaceId);
}
requestClose();
};
@ -211,15 +220,15 @@ export function HierarchyItemMenu({
pinned,
onTogglePin,
}: HierarchyItemMenuProps) {
const mx = useMatrixClient();
const ct = useClient();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const canInvite = (): boolean => {
if (!powerLevels) return false;
const creators = getRoomCreatorsForRoomId(mx, item.roomId);
const creators = ct.roomToCreators.read.get(item.roomId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
return permissions.action('invite', mx.getSafeUserId());
return permissions.action('invite', ct.session.userId);
};
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {

View file

@ -1,9 +1,14 @@
import React, { MouseEventHandler, useCallback, useMemo, useRef, useState } from 'react';
import { Box, Chip, Icon, IconButton, Icons, Line, Scroll, Spinner, Text, config } from 'folds';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useAtom, useAtomValue } from 'jotai';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useNavigate } from 'react-router-dom';
import produce from 'immer';
import { AccountDataEvent, SpaceHierarchyRoomChunk } from 'neurone/lib/api/types';
import { putStateEvent } from 'neurone/lib/api/matrix/client/v3/rooms';
import { parseMatrixResp } from 'neurone/lib/matrix-request';
import { ObjectReader } from 'neurone/lib/utils';
import { putAccountData } from 'neurone/lib/api/matrix/client/v3/user';
import { useSpace } from '../../hooks/useSpace';
import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page';
import {
@ -26,39 +31,46 @@ import {
PowerLevelsContextProvider,
usePowerLevels,
useRoomsPowerLevels,
usePowerLevelsState,
} from '../../hooks/usePowerLevels';
import { mDirectAtom } from '../../state/mDirectList';
import { makeLobbyCategoryId } from '../../state/closedLobbyCategories';
import { useCategoryHandler } from '../../hooks/useCategoryHandler';
import { allRoomsAtom } from '../../state/room-list/roomList';
import { getCanonicalAliasOrRoomId, rateLimitedActions } from '../../utils/matrix';
import { rateLimitedActions } from '../../utils/matrix';
import { getSpaceRoomPath } from '../../pages/pathUtils';
import { StateEvent } from '../../../types/matrix/room';
import { AllowCondition, StateEvent } from '../../../types/matrix/room';
import { CanDropCallback, useDnDMonitor } from './DnD';
import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable';
import { getStateEvent } from '../../utils/room';
import { useClosedLobbyCategoriesAtom } from '../../state/hooks/closedLobbyCategories';
import {
makeCinnySpacesContent,
sidebarItemWithout,
useSidebarItems,
InCinnySpacesContent,
} from '../../hooks/useSidebarItems';
import { useOrphanSpaces } from '../../state/hooks/roomList';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { AccountDataEvent } from '../../../types/matrix/accountData';
import { useRoomMembers } from '../../hooks/useRoomMembers';
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';
import { useClient } from '../../hooks/useClient';
import { RoomProvider } from '../../hooks/useRoom';
import { RoomMembersAPIProvider, useCreateRoomMembersAPI } from '../../hooks/useRoomMembers';
import { usePendingMemberLoader, usePendingMembers } from '../../hooks/usePendingMembers';
import { useAccountData } from '../../hooks/storage/useAccountData';
import { AccountDataType } from '../../../types/matrix/accountData';
import { useAllJoinedRoomsSet } from '../../hooks/useGetRoom';
import {
PowerLevelTagsContextProvider,
usePowerLevelTags,
usePowerLevelTagsState,
} from '../../hooks/usePowerLevelTags';
const useCanDropLobbyItem = (
space: Room,
roomsPowerLevels: Map<string, IPowerLevels>,
getRoom: (roomId: string) => Room | undefined
spaceId: string,
roomsPowerLevels: Map<string, IPowerLevels>
): CanDropCallback => {
const mx = useMatrixClient();
const ct = useClient();
const canDropSpace: CanDropCallback = useCallback(
(item, container) => {
@ -68,22 +80,20 @@ const useCanDropLobbyItem = (
return false;
}
const containerSpaceId = space.roomId;
const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {};
const creators = getRoomCreatorsForRoomId(mx, containerSpaceId);
const powerLevels = roomsPowerLevels.get(spaceId) ?? {};
const creators = ct.roomToCreators.read.get(spaceId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
if (
getRoom(containerSpaceId) === undefined ||
!permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
!ct.rooms.join.read.includes(spaceId) ||
!permissions.stateEvent(StateEvent.SpaceChild, ct.session.userId)
) {
return false;
}
return true;
},
[space, roomsPowerLevels, getRoom, mx]
[spaceId, roomsPowerLevels, ct]
);
const canDropRoom: CanDropCallback = useCallback(
@ -92,18 +102,18 @@ const useCanDropLobbyItem = (
'space' in container.item ? container.item.roomId : container.item.parentId;
const draggingOutsideSpace = item.parentId !== containerSpaceId;
const restrictedItem = mx.getRoom(item.roomId)?.getJoinRule() === JoinRule.Restricted;
const restrictedItem = ct.roomToJoinRule.read.get(item.roomId) === 'restricted';
// 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 itemPowerLevels = roomsPowerLevels.get(item.roomId) ?? {};
const itemCreators = getRoomCreatorsForRoomId(mx, item.roomId);
const itemCreators = ct.roomToCreators.read.get(item.roomId);
const itemPermissions = getRoomPermissionsAPI(itemCreators, itemPowerLevels);
const canChangeJoinRuleAllow = itemPermissions.stateEvent(
StateEvent.RoomJoinRules,
mx.getSafeUserId()
ct.session.userId
);
if (!canChangeJoinRuleAllow) {
return false;
@ -111,17 +121,17 @@ const useCanDropLobbyItem = (
}
const powerLevels = roomsPowerLevels.get(containerSpaceId) ?? {};
const creators = getRoomCreatorsForRoomId(mx, containerSpaceId);
const creators = ct.roomToCreators.read.get(containerSpaceId);
const permissions = getRoomPermissionsAPI(creators, powerLevels);
if (
getRoom(containerSpaceId) === undefined ||
!permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
!ct.rooms.join.read.includes(containerSpaceId) ||
!permissions.stateEvent(StateEvent.SpaceChild, ct.session.userId)
) {
return false;
}
return true;
},
[mx, getRoom, roomsPowerLevels]
[ct, roomsPowerLevels]
);
const canDrop: CanDropCallback = useCallback(
@ -145,27 +155,37 @@ const useCanDropLobbyItem = (
};
export function Lobby() {
const ct = useClient();
const navigate = useNavigate();
const mx = useMatrixClient();
const mDirects = useAtomValue(mDirectAtom);
const allRooms = useAtomValue(allRoomsAtom);
const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
const space = useSpace();
const spacePowerLevels = usePowerLevels(space);
const spaceId = useSpace();
const allJoinedRooms = useAllJoinedRoomsSet();
const powerLevelStateResult = usePowerLevelsState(spaceId);
const spacePowerLevels = usePowerLevels(powerLevelStateResult.value);
const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []);
const members = useRoomMembers(mx, space.roomId);
const powerLevelTagsStateResult = usePowerLevelTagsState(spaceId);
const powerLevelTags = usePowerLevelTags(spacePowerLevels, powerLevelTagsStateResult.value);
const membersAPI = useCreateRoomMembersAPI(spaceId);
const membersPending = usePendingMembers(spaceId);
usePendingMemberLoader(spaceId, membersPending);
const scrollRef = useRef<HTMLDivElement>(null);
const heroSectionRef = useRef<HTMLDivElement>(null);
const [heroSectionHeight, setHeroSectionHeight] = useState<number>();
const [spaceRooms, setSpaceRooms] = useAtom(spaceRoomsAtom);
const setSpaceRooms = useSetAtom(spaceRoomsAtom);
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
const screenSize = useScreenSizeContext();
const [onTop, setOnTop] = useState(true);
const [closedCategories, setClosedCategories] = useAtom(useClosedLobbyCategoriesAtom());
const [sidebarItems] = useSidebarItems(
useOrphanSpaces(mx, allRoomsAtom, useAtomValue(roomToParentsAtom))
const roomToParents = useAtomValue(roomToParentsAtom);
const orphanSpaces = useOrphanSpaces(ct, allRoomsAtom, roomToParents);
const { value: cinnySpacesEvent } = useAccountData<AccountDataEvent<InCinnySpacesContent>>(
AccountDataType.CinnySpaces
);
const sidebarItems = useSidebarItems(orphanSpaces, cinnySpacesEvent);
const sidebarSpaces = useMemo(() => {
const sideSpaces = sidebarItems.flatMap((item) => {
if (typeof item === 'string') return item;
@ -175,25 +195,23 @@ export function Lobby() {
return new Set(sideSpaces);
}, [sidebarItems]);
const [spacesItems, setSpacesItem] = useState<Map<string, IHierarchyRoom>>(() => new Map());
const [spacesItems, setSpacesItem] = useState<Map<string, SpaceHierarchyRoomChunk>>(
() => new Map()
);
useElementSizeObserver(
useCallback(() => heroSectionRef.current, []),
useCallback((w, height) => setHeroSectionHeight(height), [])
);
const getRoom = useGetRoom(allJoinedRooms);
const [draggingItem, setDraggingItem] = useState<HierarchyItem>();
const hierarchy = useSpaceHierarchy(
space.roomId,
spaceRooms,
getRoom,
spaceId,
useCallback(
(childId) =>
closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) ||
closedCategories.has(makeLobbyCategoryId(spaceId, childId)) ||
(draggingItem ? 'space' in draggingItem : false),
[closedCategories, space.roomId, draggingItem]
[closedCategories, spaceId, draggingItem]
)
);
@ -210,17 +228,17 @@ export function Lobby() {
useMemo(
() =>
hierarchy
.flatMap((i) => {
const childRooms = Array.isArray(i.rooms) ? i.rooms.map((r) => getRoom(r.roomId)) : [];
.flatMap((i): string[] => {
const childRooms = Array.isArray(i.rooms) ? i.rooms.map((r) => r.roomId) : [];
return [getRoom(i.space.roomId), ...childRooms];
return [i.space.roomId, ...childRooms];
})
.filter((r) => !!r) as Room[],
[hierarchy, getRoom]
.filter((r) => ct.rooms.join.read.includes(r)),
[hierarchy, ct]
)
);
const canDrop: CanDropCallback = useCanDropLobbyItem(space, roomsPowerLevels, getRoom);
const canDrop: CanDropCallback = useCanDropLobbyItem(spaceId, roomsPowerLevels);
const [reorderSpaceState, reorderSpace] = useAsyncCallback(
useCallback(
@ -258,25 +276,31 @@ export function Lobby() {
const parentPL = roomsPowerLevels.get(reorder.item.parentId);
if (!parentPL) return false;
const creators = getRoomCreatorsForRoomId(mx, reorder.item.parentId);
const creators = ct.roomToCreators.read.get(reorder.item.parentId);
const permissions = getRoomPermissionsAPI(creators, parentPL);
const canEdit = permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId());
const canEdit = permissions.stateEvent(StateEvent.SpaceChild, ct.session.userId);
return canEdit && reorder.orderKey !== currentOrders[index];
});
if (reorders) {
await rateLimitedActions(reorders, async (reorder) => {
if (!reorder.item.parentId) return;
await mx.sendStateEvent(
reorder.item.parentId,
StateEvent.SpaceChild as any,
{ ...reorder.item.content, order: reorder.orderKey },
reorder.item.roomId
if (!reorder.item.parentId) return undefined;
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: {
roomId: reorder.item.parentId,
eventType: StateEvent.SpaceChild,
stateKey: reorder.item.roomId,
},
body: { ...reorder.item.content, order: reorder.orderKey },
})
);
const [err] = await parseMatrixResp(resp);
return err;
});
}
},
[mx, hierarchy, lex, roomsPowerLevels]
[ct, hierarchy, lex, roomsPowerLevels]
)
);
const reorderingSpace = reorderSpaceState.status === AsyncStatus.Loading;
@ -284,7 +308,6 @@ export function Lobby() {
const [reorderRoomState, reorderRoom] = useAsyncCallback(
useCallback(
async (item: HierarchyItem, containerItem: HierarchyItem) => {
const itemRoom = mx.getRoom(item.roomId);
if (!item.parentId) {
return;
}
@ -294,49 +317,70 @@ export function Lobby() {
// remove from current space
if (item.parentId !== containerParentId) {
mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId);
ct.requestAuthJSON(
putStateEvent({
pathParams: {
roomId: item.parentId,
eventType: StateEvent.SpaceChild,
stateKey: item.roomId,
},
body: {},
})
);
}
if (
itemRoom &&
itemRoom.getJoinRule() === JoinRule.Restricted &&
ct.roomToJoinRule.read.get(item.roomId) === 'restricted' &&
item.parentId !== containerParentId
) {
// change join rule allow parameter when dragging
// restricted room from one space to another
const joinRuleContent = getStateEvent(
itemRoom,
StateEvent.RoomJoinRules
)?.getContent<RoomJoinRulesEventContent>();
const joinRuleContent = ct.state.join.joinRules.read.find(
(e) => e.roomId === item.roomId && e.event.state_key === ''
)?.event.content;
if (joinRuleContent) {
const allow =
joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ??
[];
allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, {
...joinRuleContent,
allow,
});
ObjectReader.array(joinRuleContent, 'allow')?.filter(
(allowRule) => ObjectReader.string(allowRule, 'room_id') !== item.parentId
) ?? [];
const allowCondition: AllowCondition = {
type: 'm.room_membership',
room_id: containerParentId,
};
allow.push(allowCondition);
ct.requestAuthJSON(
putStateEvent({
pathParams: {
roomId: item.roomId,
eventType: StateEvent.RoomJoinRules,
stateKey: '',
},
body: {
...joinRuleContent,
allow,
},
})
);
}
}
const itemSpaces = Array.from(
const itemRooms = Array.from(
hierarchy?.find((i) => i.space.roomId === containerParentId)?.rooms ?? []
);
).filter((i) => i.roomId !== item.roomId);
const beforeItem: HierarchyItem | undefined =
'space' in containerItem ? undefined : containerItem;
const beforeIndex = itemSpaces.findIndex((i) => i.roomId === beforeItem?.roomId);
const beforeIndex = itemRooms.findIndex((i) => i.roomId === beforeItem?.roomId);
const insertIndex = beforeIndex + 1;
itemSpaces.splice(insertIndex, 0, {
itemRooms.splice(insertIndex, 0, {
...item,
parentId: containerParentId,
content: { ...itemContent, order: undefined },
});
const currentOrders = itemSpaces.map((i) => {
const currentOrders = itemRooms.map((i) => {
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
return i.content.order;
}
@ -347,23 +391,29 @@ export function Lobby() {
const reorders = newOrders
?.map((orderKey, index) => ({
item: itemSpaces[index],
item: itemRooms[index],
orderKey,
}))
.filter((reorder, index) => reorder.item && reorder.orderKey !== currentOrders[index]);
if (reorders) {
await rateLimitedActions(reorders, async (reorder) => {
await mx.sendStateEvent(
containerParentId,
StateEvent.SpaceChild as any,
{ ...reorder.item.content, order: reorder.orderKey },
reorder.item.roomId
const resp = await ct.requestAuthJSON(
putStateEvent({
pathParams: {
roomId: containerParentId,
eventType: StateEvent.SpaceChild,
stateKey: reorder.item.roomId,
},
body: { ...reorder.item.content, order: reorder.orderKey },
})
);
const [err] = await parseMatrixResp(resp);
return err;
});
}
},
[mx, hierarchy, lex]
[ct, hierarchy, lex]
)
);
const reorderingRoom = reorderRoomState.status === AsyncStatus.Loading;
@ -388,7 +438,7 @@ export function Lobby() {
);
const handleSpacesFound = useCallback(
(sItems: IHierarchyRoom[]) => {
(sItems: SpaceHierarchyRoomChunk[]) => {
setSpaceRooms({ type: 'PUT', roomIds: sItems.map((i) => i.room_id) });
setSpacesItem((current) => {
const newItems = produce(current, (draft) => {
@ -407,8 +457,10 @@ export function Lobby() {
const handleOpenRoom: MouseEventHandler<HTMLButtonElement> = (evt) => {
const rId = evt.currentTarget.getAttribute('data-room-id');
if (!rId) return;
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId);
navigate(getSpaceRoomPath(pSpaceIdOrAlias, getCanonicalAliasOrRoomId(mx, rId)));
const spaceAlias = ct.roomToAlias.read.get(spaceId);
const roomAlias = ct.roomToAlias.read.get(rId);
navigate(getSpaceRoomPath(spaceAlias ?? spaceId, roomAlias ?? rId));
};
const togglePinToSidebar = useCallback(
@ -417,127 +469,134 @@ export function Lobby() {
if (!sidebarSpaces.has(rId)) {
newItems.push(rId);
}
const newSpacesContent = makeCinnySpacesContent(mx, newItems);
mx.setAccountData(AccountDataEvent.CinnySpaces as any, newSpacesContent as any);
const newSpacesContent = makeCinnySpacesContent(newItems, cinnySpacesEvent);
ct.requestAuthJSON(
putAccountData({
pathParams: {
userId: ct.session.userId,
type: AccountDataType.CinnySpaces,
},
body: newSpacesContent,
})
);
},
[mx, sidebarItems, sidebarSpaces]
[ct, sidebarItems, sidebarSpaces, cinnySpacesEvent]
);
return (
<PowerLevelsContextProvider value={spacePowerLevels}>
<Box grow="Yes">
<Page>
<LobbyHeader
showProfile={!onTop}
powerLevels={roomsPowerLevels.get(space.roomId) ?? {}}
/>
<Box style={{ position: 'relative' }} grow="Yes">
<Scroll ref={scrollRef} hideTrack visibility="Hover">
<PageContent>
<PageContentCenter>
<ScrollTopContainer
scrollRef={scrollRef}
anchorRef={heroSectionRef}
onVisibilityChange={setOnTop}
>
<IconButton
onClick={() => virtualizer.scrollToOffset(0)}
variant="SurfaceVariant"
radii="Pill"
outlined
size="300"
aria-label="Scroll to Top"
>
<Icon src={Icons.ChevronTop} size="300" />
</IconButton>
</ScrollTopContainer>
<div
style={{
position: 'relative',
height: virtualizer.getTotalSize(),
}}
>
<PageHeroSection ref={heroSectionRef} style={{ paddingTop: 0 }}>
<LobbyHero />
</PageHeroSection>
{vItems.map((vItem) => {
const item = hierarchy[vItem.index];
if (!item) return null;
const nextSpaceId = hierarchy[vItem.index + 1]?.space.roomId;
const categoryId = makeLobbyCategoryId(space.roomId, item.space.roomId);
return (
<VirtualTile
virtualItem={vItem}
style={{
paddingTop: vItem.index === 0 ? 0 : config.space.S500,
}}
ref={virtualizer.measureElement}
key={vItem.index}
>
<SpaceHierarchy
spaceItem={item.space}
summary={spacesItems.get(item.space.roomId)}
roomItems={item.rooms}
allJoinedRooms={allJoinedRooms}
mDirects={mDirects}
roomsPowerLevels={roomsPowerLevels}
categoryId={categoryId}
closed={
closedCategories.has(categoryId) ||
(draggingItem ? 'space' in draggingItem : false)
}
handleClose={handleCategoryClick}
draggingItem={draggingItem}
onDragging={setDraggingItem}
canDrop={canDrop}
disabledReorder={reordering}
nextSpaceId={nextSpaceId}
getRoom={getRoom}
pinned={sidebarSpaces.has(item.space.roomId)}
togglePinToSidebar={togglePinToSidebar}
onSpacesFound={handleSpacesFound}
onOpenRoom={handleOpenRoom}
/>
</VirtualTile>
);
})}
</div>
{reordering && (
<Box
style={{
position: 'absolute',
bottom: config.space.S400,
left: 0,
right: 0,
zIndex: 2,
pointerEvents: 'none',
}}
justifyContent="Center"
>
<Chip
variant="Secondary"
outlined
radii="Pill"
before={<Spinner variant="Secondary" fill="Soft" size="100" />}
<RoomMembersAPIProvider value={membersAPI}>
<PowerLevelsContextProvider value={spacePowerLevels}>
<PowerLevelTagsContextProvider value={powerLevelTags}>
<Box grow="Yes">
<Page>
<LobbyHeader showProfile={!onTop} powerLevels={roomsPowerLevels.get(spaceId) ?? {}} />
<Box style={{ position: 'relative' }} grow="Yes">
<Scroll ref={scrollRef} hideTrack visibility="Hover">
<PageContent>
<PageContentCenter>
<ScrollTopContainer
scrollRef={scrollRef}
anchorRef={heroSectionRef}
onVisibilityChange={setOnTop}
>
<Text size="L400">Reordering</Text>
</Chip>
</Box>
)}
</PageContentCenter>
</PageContent>
</Scroll>
<IconButton
onClick={() => virtualizer.scrollToOffset(0)}
variant="SurfaceVariant"
radii="Pill"
outlined
size="300"
aria-label="Scroll to Top"
>
<Icon src={Icons.ChevronTop} size="300" />
</IconButton>
</ScrollTopContainer>
<div
style={{
position: 'relative',
height: virtualizer.getTotalSize(),
}}
>
<PageHeroSection ref={heroSectionRef} style={{ paddingTop: 0 }}>
<LobbyHero />
</PageHeroSection>
{vItems.map((vItem) => {
const item = hierarchy[vItem.index];
if (!item) return null;
const nextSpaceId = hierarchy[vItem.index + 1]?.space.roomId;
const categoryId = makeLobbyCategoryId(spaceId, item.space.roomId);
return (
<VirtualTile
virtualItem={vItem}
style={{
paddingTop: vItem.index === 0 ? 0 : config.space.S500,
}}
ref={virtualizer.measureElement}
key={vItem.index}
>
<SpaceHierarchy
spaceItem={item.space}
summary={spacesItems.get(item.space.roomId)}
roomItems={item.rooms}
allJoinedRooms={allJoinedRooms}
roomsPowerLevels={roomsPowerLevels}
categoryId={categoryId}
closed={
closedCategories.has(categoryId) ||
(draggingItem ? 'space' in draggingItem : false)
}
handleClose={handleCategoryClick}
draggingItem={draggingItem}
onDragging={setDraggingItem}
canDrop={canDrop}
disabledReorder={reordering}
nextSpaceId={nextSpaceId}
pinned={sidebarSpaces.has(item.space.roomId)}
togglePinToSidebar={togglePinToSidebar}
onSpacesFound={handleSpacesFound}
onOpenRoom={handleOpenRoom}
/>
</VirtualTile>
);
})}
</div>
{reordering && (
<Box
style={{
position: 'absolute',
bottom: config.space.S400,
left: 0,
right: 0,
zIndex: 2,
pointerEvents: 'none',
}}
justifyContent="Center"
>
<Chip
variant="Secondary"
outlined
radii="Pill"
before={<Spinner variant="Secondary" fill="Soft" size="100" />}
>
<Text size="L400">Reordering</Text>
</Chip>
</Box>
)}
</PageContentCenter>
</PageContent>
</Scroll>
</Box>
</Page>
{screenSize === ScreenSize.Desktop && isDrawer && (
<RoomProvider value={spaceId}>
<Line variant="Background" direction="Vertical" size="300" />
<MembersDrawer />
</RoomProvider>
)}
</Box>
</Page>
{screenSize === ScreenSize.Desktop && isDrawer && (
<>
<Line variant="Background" direction="Vertical" size="300" />
<MembersDrawer room={space} members={members} />
</>
)}
</Box>
</PowerLevelsContextProvider>
</PowerLevelTagsContextProvider>
</PowerLevelsContextProvider>
</RoomMembersAPIProvider>
);
}

View file

@ -20,9 +20,7 @@ import FocusTrap from 'focus-trap-react';
import { PageHeader } from '../../components/page';
import { useSetSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { useRoomAvatar, useRoomName } from '../../hooks/useRoomMeta';
import { useSpace } from '../../hooks/useSpace';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { RoomAvatar } from '../../components/room-avatar';
import { nameInitials } from '../../utils/common';
import * as css from './LobbyHeader.css';
@ -32,12 +30,16 @@ import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
import { stopPropagation } from '../../utils/keyboard';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { BackRouteHandler } from '../../components/BackRouteHandler';
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';
import { InviteUserPrompt } from '../../components/invite-user-prompt';
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';
type LobbyMenuProps = {
powerLevels: IPowerLevels;
@ -45,12 +47,12 @@ type LobbyMenuProps = {
};
const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
({ powerLevels, requestClose }, ref) => {
const mx = useMatrixClient();
const space = useSpace();
const creators = useRoomCreators(space);
const ct = useClient();
const spaceId = useSpace();
const creators = useRoomCreators(spaceId);
const permissions = useRoomPermissions(creators, powerLevels);
const canInvite = permissions.action('invite', mx.getSafeUserId());
const canInvite = permissions.action('invite', ct.session.userId);
const openSpaceSettings = useOpenSpaceSettings();
const [invitePrompt, setInvitePrompt] = useState(false);
@ -60,7 +62,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
};
const handleRoomSettings = () => {
openSpaceSettings(space.roomId);
openSpaceSettings(spaceId);
requestClose();
};
@ -68,7 +70,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
{invitePrompt && (
<InviteUserPrompt
room={space}
roomId={spaceId}
requestClose={() => {
setInvitePrompt(false);
requestClose();
@ -121,7 +123,7 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
</MenuItem>
{promptLeave && (
<LeaveSpacePrompt
roomId={space.roomId}
roomId={spaceId}
onDone={requestClose}
onCancel={() => setPromptLeave(false)}
/>
@ -140,19 +142,28 @@ type LobbyHeaderProps = {
powerLevels: IPowerLevels;
};
export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
const mx = useMatrixClient();
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const space = useSpace();
const spaceId = useSpace();
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const screenSize = useScreenSizeContext();
const name = useRoomName(space);
const avatarMxc = useRoomAvatar(space);
const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(spaceId);
const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
const avatarMxc = getRoomAvatar(spaceId);
const avatarUrl = avatarMxc
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
? mxcToHttp(ct.session.baseUrl, avatarMxc, useAuthentication).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined;
const name = getRoomName(spaceId) ?? 'Unknown';
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
};
@ -187,7 +198,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
<>
<Avatar size="300">
<RoomAvatar
roomId={space.roomId}
roomId={spaceId}
src={avatarUrl}
alt={name}
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}

View file

@ -1,9 +1,7 @@
import React from 'react';
import { Avatar, Overlay, OverlayBackdrop, OverlayCenter, Text } from 'folds';
import FocusTrap from 'focus-trap-react';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { useSpace } from '../../hooks/useSpace';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { RoomAvatar } from '../../components/room-avatar';
import { nameInitials } from '../../utils/common';
import { UseStateProvider } from '../../components/UseStateProvider';
@ -11,25 +9,41 @@ import { RoomTopicViewer } from '../../components/room-topic-viewer';
import * as css from './LobbyHero.css';
import { PageHero } from '../../components/page';
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useClient } from '../../hooks/useClient';
import { useRoomHeroes } from '../../hooks/storage/useRoomHeros';
import { useGetRoomName } from '../../hooks/storage/useGetRoomName';
import { useGetRoomAvatar } from '../../hooks/storage/useGetRoomAvatar';
import { useRoomTopic } from '../../hooks/storage/useRoomTopic';
import { mxcToHttp } from '../../../client/mxcToHttp';
export function LobbyHero() {
const mx = useMatrixClient();
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const space = useSpace();
const spaceId = useSpace();
const name = useRoomName(space);
const topic = useRoomTopic(space);
const avatarMxc = useRoomAvatar(space);
const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined;
const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(spaceId);
const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
const topic = useRoomTopic(spaceId);
const avatarMxc = getRoomAvatar(spaceId);
const avatarUrl = avatarMxc
? mxcToHttp(ct.session.baseUrl, avatarMxc, useAuthentication).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined;
const name = getRoomName(spaceId) ?? 'Unknown';
return (
<PageHero
icon={
<Avatar size="500">
<RoomAvatar
roomId={space.roomId}
roomId={spaceId}
src={avatarUrl}
alt={name}
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}

View file

@ -1,4 +1,4 @@
import React, { MouseEventHandler, ReactNode, useCallback, useRef } from 'react';
import React, { MouseEventHandler, ReactNode, useCallback, useRef, useState } from 'react';
import {
Avatar,
Badge,
@ -13,72 +13,90 @@ import {
Spinner,
Text,
Tooltip,
TooltipProvider,
as,
color,
toRem,
PopOut,
RectCords,
IconButton,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { SpaceHierarchyRoomChunk } from 'neurone/lib/api/types';
import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
import { postJoin } from 'neurone/lib/api/matrix/client/v3/join';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { SequenceCard } from '../../components/sequence-card';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import { millify } from '../../plugins/millify';
import { LocalRoomSummaryLoader } from '../../components/RoomSummaryLoader';
import { UseStateProvider } from '../../components/UseStateProvider';
import { RoomTopicViewer } from '../../components/room-topic-viewer';
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
import { Membership } from '../../../types/matrix/room';
import { JoinRule } from '../../../types/matrix/room';
import * as css from './RoomItem.css';
import * as styleCss from './style.css';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
import { ItemDraggableTarget, useDraggableItem } from './DnD';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useClient } from '../../hooks/useClient';
import { BreakWord } from '../../styles/Text.css';
import { mxcToHttp } from '../../../client/mxcToHttp';
type RoomJoinButtonProps = {
roomId: string;
via?: string[];
};
function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
const mx = useMatrixClient();
const ct = useClient();
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
useCallback(() => mx.joinRoom(roomId, { viaServers: via }), [mx, roomId, via])
const [joinState, join] = useAsyncCallback(
useCallback(async () => {
const resp = await ct.requestAuthJSON(
postJoin({
pathParams: { roomIdOrAlias: roomId },
searchParams: { via, server_name: via },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(printMatrixError(err));
}, [ct, roomId, via])
);
const canJoin = joinState.status === AsyncStatus.Idle || joinState.status === AsyncStatus.Error;
const [cords, setCords] = useState<RectCords>();
const handleErrorMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setCords(evt.currentTarget.getBoundingClientRect());
};
return (
<Box shrink="No" gap="200" alignItems="Center">
{joinState.status === AsyncStatus.Error && (
<TooltipProvider
tooltip={
<PopOut
anchor={cords}
content={
<Tooltip variant="Critical" style={{ maxWidth: toRem(200) }}>
<Box direction="Column" gap="100">
<Text style={{ wordBreak: 'break-word' }} size="T400">
{joinState.error.data?.error || joinState.error.message}
<Text className={BreakWord} size="T400">
{typeof joinState.error === 'object' && joinState.error instanceof Error
? joinState.error.message
: 'Unknown error'}
</Text>
<Text size="T200">{joinState.error.name}</Text>
</Box>
</Tooltip>
}
>
{(triggerRef) => (
<IconButton variant="SurfaceVariant" radii="300" size="300" onClick={handleErrorMenu}>
<Icon
ref={triggerRef}
style={{ color: color.Critical.Main, cursor: 'pointer' }}
src={Icons.Warning}
size="400"
size="200"
filled
tabIndex={0}
aria-label={joinState.error.data?.error || joinState.error.message}
aria-label="Join room error message"
/>
)}
</TooltipProvider>
</IconButton>
</PopOut>
)}
<Chip
variant="Secondary"
@ -135,11 +153,7 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
src={undefined}
alt={roomId}
renderFallback={() => (
<RoomIcon
size="300"
joinRule={inaccessibleRoom ? JoinRule.Invite : JoinRule.Restricted}
filled
/>
<RoomIcon size="300" joinRule={inaccessibleRoom ? 'invite' : 'restricted'} filled />
)}
/>
</Avatar>
@ -279,18 +293,17 @@ function RoomProfile({
type RoomItemCardProps = {
item: HierarchyItem;
loading: boolean;
error: Error | null;
summary: IHierarchyRoom | undefined;
dm?: boolean;
error: unknown;
summary: SpaceHierarchyRoomChunk | undefined;
firstChild?: boolean;
lastChild?: boolean;
onOpen: MouseEventHandler<HTMLButtonElement>;
joined?: boolean;
options?: ReactNode;
before?: ReactNode;
after?: ReactNode;
onDragging: (item?: HierarchyItem) => void;
canReorder: boolean;
getRoom: (roomId: string) => Room | undefined;
};
export const RoomItemCard = as<'div', RoomItemCardProps>(
(
@ -299,28 +312,24 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
loading,
error,
summary,
dm,
onOpen,
joined,
options,
before,
after,
onDragging,
canReorder,
getRoom,
...props
},
ref
) => {
const mx = useMatrixClient();
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const { roomId, content } = item;
const room = getRoom(roomId);
const targetRef = useRef<HTMLDivElement>(null);
const targetHandleRef = useRef<HTMLDivElement>(null);
useDraggableItem(item, targetRef, onDragging, targetHandleRef);
const joined = room?.getMyMembership() === Membership.Join;
return (
<SequenceCard
className={css.RoomItemCard}
@ -333,18 +342,26 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
{before}
<Box ref={canReorder ? targetRef : null} grow="Yes">
{canReorder && <ItemDraggableTarget ref={targetHandleRef} />}
{room ? (
<LocalRoomSummaryLoader room={room}>
{joined ? (
<LocalRoomSummaryLoader roomId={roomId}>
{(localSummary) => (
<RoomProfile
roomId={roomId}
roomType={localSummary.roomType}
name={localSummary.name}
name={localSummary.name ?? 'Unknown'}
topic={localSummary.topic}
avatarUrl={
dm
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
localSummary.avatarUrl
? mxcToHttp(
ct.session.baseUrl,
localSummary.avatarUrl,
useAuthentication
).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined
}
memberCount={localSummary.memberCount}
suggested={content.suggested}
@ -402,13 +419,20 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
topic={summary.topic}
avatarUrl={
summary?.avatar_url
? mxcUrlToHttp(mx, summary.avatar_url, useAuthentication, 96, 96, 'crop') ??
undefined
? mxcToHttp(
ct.session.baseUrl,
summary.avatar_url,
useAuthentication
).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined
}
memberCount={summary.num_joined_members}
suggested={content.suggested}
joinRule={summary.join_rule}
joinRule={summary.join_rule as JoinRule}
options={<RoomJoinButton roomId={roomId} via={content.via} />}
/>
)}

View file

@ -1,7 +1,6 @@
import React, { forwardRef, MouseEventHandler, useEffect, useMemo } from 'react';
import { MatrixError, Room } from 'matrix-js-sdk';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { Box, config, Text } from 'folds';
import { SpaceHierarchyRoomChunk } from 'neurone/lib/api/types';
import {
HierarchyItem,
HierarchyItemRoom,
@ -9,22 +8,22 @@ import {
useFetchSpaceHierarchyLevel,
} from '../../hooks/useSpaceHierarchy';
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, StateEvent } from '../../../types/matrix/room';
import { SequenceCard } from '../../components/sequence-card';
import { getRoomCreatorsForRoomId } from '../../hooks/useRoomCreators';
import { getRoomPermissionsAPI } from '../../hooks/useRoomPermissions';
import { useClient } from '../../hooks/useClient';
import { rError } from '../../../client/reader';
import { ErrorCode } from '../../cs-errorcode';
type SpaceHierarchyProps = {
summary: IHierarchyRoom | undefined;
summary: SpaceHierarchyRoomChunk | undefined;
spaceItem: HierarchyItemSpace;
roomItems?: HierarchyItemRoom[];
allJoinedRooms: Set<string>;
mDirects: Set<string>;
roomsPowerLevels: Map<string, IPowerLevels>;
categoryId: string;
closed: boolean;
@ -34,10 +33,9 @@ type SpaceHierarchyProps = {
canDrop: CanDropCallback;
disabledReorder?: boolean;
nextSpaceId?: string;
getRoom: (roomId: string) => Room | undefined;
pinned: boolean;
togglePinToSidebar: (roomId: string) => void;
onSpacesFound: (spaceItems: IHierarchyRoom[]) => void;
onSpacesFound: (spaceItems: SpaceHierarchyRoomChunk[]) => void;
onOpenRoom: MouseEventHandler<HTMLButtonElement>;
};
export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
@ -47,7 +45,6 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
spaceItem,
roomItems,
allJoinedRooms,
mDirects,
roomsPowerLevels,
categoryId,
closed,
@ -57,7 +54,6 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
canDrop,
disabledReorder,
nextSpaceId,
getRoom,
pinned,
togglePinToSidebar,
onOpenRoom,
@ -65,12 +61,11 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
},
ref
) => {
const mx = useMatrixClient();
const ct = useClient();
const { fetching, error, rooms } = useFetchSpaceHierarchyLevel(spaceItem.roomId, true);
const subspaces = useMemo(() => {
const s: Map<string, IHierarchyRoom> = new Map();
const s: Map<string, SpaceHierarchyRoomChunk> = new Map();
rooms.forEach((r) => {
if (r.room_type === RoomType.Space) {
s.set(r.room_id, r);
@ -80,7 +75,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
}, [rooms]);
const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId);
const spaceCreators = getRoomCreatorsForRoomId(mx, spaceItem.roomId);
const spaceCreators = ct.roomToCreators.read.get(spaceItem.roomId);
const spacePermissions =
spacePowerLevels && getRoomPermissionsAPI(spaceCreators, spacePowerLevels);
@ -89,21 +84,19 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
const { parentId } = spaceItem;
const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) : undefined;
const parentCreators = parentId ? getRoomCreatorsForRoomId(mx, parentId) : undefined;
const parentCreators = parentId ? ct.roomToCreators.read.get(parentId) : undefined;
const parentPermissions =
parentCreators &&
parentPowerLevels &&
getRoomPermissionsAPI(parentCreators, parentPowerLevels);
parentPowerLevels && getRoomPermissionsAPI(parentCreators, parentPowerLevels);
useEffect(() => {
onSpacesFound(Array.from(subspaces.values()));
}, [subspaces, onSpacesFound]);
let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId));
if (!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())) {
if (!spacePermissions?.stateEvent(StateEvent.SpaceChild, ct.session.userId)) {
// hide unknown rooms for normal user
childItems = childItems?.filter((i) => {
const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false;
const forbidden = rError.errcode(error) === ErrorCode.M_FORBIDDEN;
const inaccessibleRoom = !rooms.get(i.roomId) && !fetching && (error ? forbidden : true);
return !inaccessibleRoom;
});
@ -119,11 +112,10 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
categoryId={categoryId}
closed={closed}
handleClose={handleClose}
getRoom={getRoom}
canEditChild={!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())}
canEditChild={!!spacePermissions?.stateEvent(StateEvent.SpaceChild, ct.session.userId)}
canReorder={
parentPowerLevels && !disabledReorder && parentPermissions
? parentPermissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
!disabledReorder && parentPermissions
? parentPermissions.stateEvent(StateEvent.SpaceChild, ct.session.userId)
: false
}
options={
@ -134,7 +126,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
powerLevels={spacePowerLevels}
joined={allJoinedRooms.has(spaceItem.roomId)}
canEditChild={
!!parentPermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
!!parentPermissions?.stateEvent(StateEvent.SpaceChild, ct.session.userId)
}
pinned={pinned}
onTogglePin={togglePinToSidebar}
@ -159,8 +151,8 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
const roomPowerLevels = roomsPowerLevels.get(roomItem.roomId) ?? {};
const lastItem = index === childItems.length;
const nextRoomId = lastItem ? nextSpaceId : childItems[index + 1]?.roomId;
const lastItem = index === childItems?.length;
const nextRoomId = lastItem ? nextSpaceId : childItems?.[index + 1]?.roomId;
const roomDragging =
draggingItem?.roomId === roomItem.roomId &&
@ -173,11 +165,10 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
loading={fetching}
error={error}
summary={roomSummary}
dm={mDirects.has(roomItem.roomId)}
onOpen={onOpenRoom}
getRoom={getRoom}
joined={allJoinedRooms.has(roomItem.roomId)}
canReorder={
!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId()) &&
!!spacePermissions?.stateEvent(StateEvent.SpaceChild, ct.session.userId) &&
!disabledReorder
}
options={
@ -186,7 +177,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
powerLevels={roomPowerLevels}
joined={allJoinedRooms.has(roomItem.roomId)}
canEditChild={
!!spacePermissions?.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
!!spacePermissions?.stateEvent(StateEvent.SpaceChild, ct.session.userId)
}
/>
}

View file

@ -1,4 +1,11 @@
import React, { MouseEventHandler, ReactNode, useCallback, useRef, useState } from 'react';
import React, {
Fragment,
MouseEventHandler,
ReactNode,
useCallback,
useRef,
useState,
} from 'react';
import {
Box,
Avatar,
@ -18,26 +25,28 @@ import {
} from 'folds';
import FocusTrap from 'focus-trap-react';
import classNames from 'classnames';
import { MatrixError, Room } from 'matrix-js-sdk';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { postJoin } from 'neurone/lib/api/matrix/client/v3/join';
import { parseMatrixResp, printMatrixError } from 'neurone/lib/matrix-request';
import { SpaceHierarchyRoomChunk } from 'neurone/lib/api/types';
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { RoomAvatar } from '../../components/room-avatar';
import { nameInitials } from '../../utils/common';
import { LocalRoomSummaryLoader } from '../../components/RoomSummaryLoader';
import { getRoomAvatarUrl } from '../../utils/room';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import * as css from './SpaceItem.css';
import * as styleCss from './style.css';
import { useDraggableItem } from './DnD';
import { stopPropagation } from '../../utils/keyboard';
import { mxcUrlToHttp } from '../../utils/matrix';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
import { AddExistingModal } from '../add-existing';
import { CreateRoomType } from '../../components/create-room/types';
import { BetaNoticeBadge } from '../../components/BetaNoticeBadge';
import { useClient } from '../../hooks/useClient';
import { useGetRoomName } from '../../hooks/storage/useGetRoomName';
import { useRoomHeroes } from '../../hooks/storage/useRoomHeros';
import { useGetRoomAvatar } from '../../hooks/storage/useGetRoomAvatar';
import { mxcToHttp } from '../../../client/mxcToHttp';
function SpaceProfileLoading() {
return (
@ -110,10 +119,19 @@ function UnjoinedSpaceProfile({
avatarUrl,
suggested,
}: UnjoinedSpaceProfileProps) {
const mx = useMatrixClient();
const ct = useClient();
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
useCallback(() => mx.joinRoom(roomId, { viaServers: via }), [mx, roomId, via])
const [joinState, join] = useAsyncCallback(
useCallback(async () => {
const resp = await ct.requestAuthJSON(
postJoin({
pathParams: { roomIdOrAlias: roomId },
searchParams: { via, server_name: via },
})
);
const [err] = await parseMatrixResp(resp);
if (err) throw new Error(printMatrixError(err));
}, [ct, roomId, via])
);
const canJoin = joinState.status === AsyncStatus.Idle || joinState.status === AsyncStatus.Error;
@ -154,7 +172,7 @@ function UnjoinedSpaceProfile({
{joinState.status === AsyncStatus.Error && (
<Badge variant="Critical" fill="Soft" radii="Pill" outlined>
<Text size="L400" truncate>
{joinState.error.name}
Error
</Text>
</Badge>
)}
@ -165,22 +183,28 @@ function UnjoinedSpaceProfile({
type SpaceProfileProps = {
roomId: string;
name: string;
avatarUrl?: string;
suggested?: boolean;
closed: boolean;
categoryId: string;
handleClose?: MouseEventHandler<HTMLButtonElement>;
};
function SpaceProfile({
roomId,
name,
avatarUrl,
suggested,
closed,
categoryId,
handleClose,
}: SpaceProfileProps) {
function SpaceProfile({ roomId, suggested, closed, categoryId, handleClose }: SpaceProfileProps) {
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const [getRoomHeroes, roomToHeroMembersResult] = useRoomHeroes(roomId);
const getRoomName = useGetRoomName(getRoomHeroes, roomToHeroMembersResult.value);
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
const name = getRoomName(roomId) ?? 'Unknown';
const avatar = getRoomAvatar(roomId);
const avatarUrl = avatar
? mxcToHttp(ct.session.baseUrl, avatar, useAuthentication).thumbnail({
width: 96,
height: 96,
method: 'crop',
})
: undefined;
return (
<Chip
data-category-id={categoryId}
@ -330,7 +354,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
};
const handleCreateSpace = () => {
openCreateSpaceModal(item.roomId as any);
openCreateSpaceModal(item.roomId);
setCords(undefined);
};
@ -388,7 +412,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
}
type SpaceItemCardProps = {
summary: IHierarchyRoom | undefined;
summary: SpaceHierarchyRoomChunk | undefined;
loading?: boolean;
item: HierarchyItem;
joined?: boolean;
@ -401,7 +425,6 @@ type SpaceItemCardProps = {
canEditChild: boolean;
canReorder: boolean;
onDragging: (item?: HierarchyItem) => void;
getRoom: (roomId: string) => Room | undefined;
};
export const SpaceItemCard = as<'div', SpaceItemCardProps>(
(
@ -420,15 +443,13 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
canEditChild,
canReorder,
onDragging,
getRoom,
...props
},
ref
) => {
const mx = useMatrixClient();
const ct = useClient();
const useAuthentication = useMediaAuthentication();
const { roomId, content } = item;
const space = getRoom(roomId);
const targetRef = useRef<HTMLDivElement>(null);
useDraggableItem(item, targetRef, onDragging);
@ -444,28 +465,25 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
{before}
<Box grow="Yes" gap="100" alignItems="Inherit" justifyContent="SpaceBetween">
<Box ref={canReorder ? targetRef : null}>
{space ? (
<LocalRoomSummaryLoader room={space}>
{(localSummary) =>
item.parentId ? (
<SpaceProfile
roomId={roomId}
name={localSummary.name}
avatarUrl={getRoomAvatarUrl(mx, space, 96, useAuthentication)}
suggested={content.suggested}
closed={closed}
categoryId={categoryId}
handleClose={handleClose}
/>
) : (
<RootSpaceProfile
closed={closed}
categoryId={categoryId}
handleClose={handleClose}
/>
)
}
</LocalRoomSummaryLoader>
{joined ? (
<>
{item.parentId && (
<SpaceProfile
roomId={roomId}
suggested={content.suggested}
closed={closed}
categoryId={categoryId}
handleClose={handleClose}
/>
)}
{!item.parentId && (
<RootSpaceProfile
closed={closed}
categoryId={categoryId}
handleClose={handleClose}
/>
)}
</>
) : (
<>
{!summary &&
@ -484,8 +502,11 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
name={summary.name || summary.canonical_alias || roomId}
avatarUrl={
summary?.avatar_url
? mxcUrlToHttp(mx, summary.avatar_url, useAuthentication, 96, 96, 'crop') ??
undefined
? mxcToHttp(
ct.session.baseUrl,
summary.avatar_url,
useAuthentication
).thumbnail({ width: 96, height: 96, method: 'crop' }) ?? undefined
: undefined
}
suggested={content.suggested}
@ -494,7 +515,7 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
</>
)}
</Box>
{space && canEditChild && (
{joined && canEditChild && (
<Box shrink="No" alignItems="Inherit" gap="200">
<AddRoomButton item={item} />
{item.parentId === undefined && <AddSpaceButton item={item} />}

View file

@ -318,6 +318,7 @@ export function RoomNavItem({
filled={selected}
size="100"
joinRule={joinRule}
roomType={ct.roomToType.read.get(roomId)}
/>
)}
</Avatar>

View file

@ -341,7 +341,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
src={avatarUrl}
alt={name}
renderFallback={() => (
<RoomIcon size="200" joinRule={roomToJoinRule.get(roomId)} filled />
<RoomIcon
size="200"
joinRule={roomToJoinRule.get(roomId)}
roomType={ct.roomToType.read.get(roomId)}
filled
/>
)}
/>
</Avatar>

View file

@ -13,7 +13,7 @@ export type LocalRoomSummary = {
avatarUrl?: string;
canonicalAlias?: string;
memberCount?: number;
space?: boolean;
roomType?: string;
joinRule?: JoinRule;
};
export const useLocalRoomSummary = (roomId: string): LocalRoomSummary => {
@ -25,7 +25,7 @@ export const useLocalRoomSummary = (roomId: string): LocalRoomSummary => {
const roomToAlias = useObservableValue(ct.roomToAlias);
const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
const roomToSummary = useObservableValue(ct.roomToSummary);
const spaceRooms = useObservableValue(ct.spaceRooms);
const roomToType = useObservableValue(ct.roomToType);
return {
roomId,
@ -34,7 +34,7 @@ export const useLocalRoomSummary = (roomId: string): LocalRoomSummary => {
avatarUrl: getRoomAvatar(roomId),
canonicalAlias: roomToAlias.get(roomId),
memberCount: roomToSummary.get(roomId)?.['m.joined_member_count'],
space: spaceRooms.has(roomId),
roomType: roomToType.get(roomId),
joinRule: roomToJoinRule.get(roomId),
};
};

View file

@ -1,11 +1,8 @@
import { MatrixEvent, Room } from 'matrix-js-sdk';
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { MatrixEvent } from 'matrix-js-sdk';
import { createContext, useCallback, useContext, useMemo } from 'react';
import produce from 'immer';
import { PendingObservableResult } from 'neurone/lib/utils/observable';
import { StateEvent, StateEventType } from '../../types/matrix/room';
import { useStateEventCallback } from './useStateEventCallback';
import { useMatrixClient } from './useMatrixClient';
import { getStateEvent } from '../utils/room';
import { getJoinRoomStateId, JoinRoomStateObject } from '../../client/storage/collections';
import { useClient } from './useClient';
import { useObservableValuePromise } from './useObservableValue';
@ -84,12 +81,14 @@ export function usePowerLevelsState(
return useObservableValuePromise(eventObservablePromise);
}
export function usePowerLevels(eventObj: JoinRoomStateObject | undefined): IPowerLevels {
const powerLevels: IPowerLevels = useMemo(() => {
const content: IPowerLevels | undefined = eventObj?.event.content;
function parsePowerLevels(eventObj?: JoinRoomStateObject): IPowerLevels {
const content: IPowerLevels | undefined = eventObj?.event.content;
return !content ? DEFAULT_POWER_LEVELS : fillMissingPowers(content);
}, [eventObj]);
return !content ? DEFAULT_POWER_LEVELS : fillMissingPowers(content);
}
export function usePowerLevels(eventObj: JoinRoomStateObject | undefined): IPowerLevels {
const powerLevels: IPowerLevels = useMemo(() => parsePowerLevels(eventObj), [eventObj]);
return powerLevels;
}
@ -104,38 +103,27 @@ export const usePowerLevelsContext = (): IPowerLevels => {
return pl;
};
export const useRoomsPowerLevels = (rooms: Room[]): Map<string, IPowerLevels> => {
const mx = useMatrixClient();
const getRoomsPowerLevels = useCallback(() => {
const rToPl = new Map<string, IPowerLevels>();
export const useRoomsPowerLevels = (rooms: string[]): Map<string, IPowerLevels> => {
const ct = useClient();
const powerLevelEventsPromise = useMemo(
() => ct.storage.joinRoomState.observableRead.queryIndex('type', StateEvent.RoomPowerLevels),
[ct]
);
const plEventsResult = useObservableValuePromise(powerLevelEventsPromise);
rooms.forEach((room) => {
const mEvent = getStateEvent(room, StateEvent.RoomPowerLevels, '');
rToPl.set(room.roomId, getPowersLevelFromMatrixEvent(mEvent));
const roomToPowerLevels: Map<string, IPowerLevels> = useMemo(() => {
const rToP = new Map<string, IPowerLevels>();
console.log(plEventsResult.value?.find((v) => v.roomId === '!KqyBSJuhpIHbWaROMB:kde.org'));
rooms.forEach((roomId) => {
const evtObj = plEventsResult.value?.find(
(o) => o.roomId === roomId && o.event.state_key === ''
);
rToP.set(roomId, parsePowerLevels(evtObj));
});
return rToPl;
}, [rooms]);
const [roomToPowerLevels, setRoomToPowerLevels] = useState(() => getRoomsPowerLevels());
useStateEventCallback(
mx,
useCallback(
(event) => {
const roomId = event.getRoomId();
if (
roomId &&
event.getType() === StateEvent.RoomPowerLevels &&
event.getStateKey() === '' &&
rooms.find((r) => r.roomId === roomId)
) {
setRoomToPowerLevels(getRoomsPowerLevels());
}
},
[rooms, getRoomsPowerLevels]
)
);
return rToP;
}, [plEventsResult, rooms]);
return roomToPowerLevels;
};

View file

@ -95,7 +95,8 @@ const getSpaceHierarchy = (
rootSpaceId: string,
roomToChildEvents: Map<string, StateEventWithoutRoomId<SpaceChildContent>[]>,
spaceRooms: Set<string>,
spaceRoomsCache: Set<string>
spaceRoomsCache: Set<string>,
closedCategory: (spaceId: string) => boolean
): SpaceHierarchy[] => {
const spaceItems: HierarchyItemSpace[] = getHierarchySpaces(
rootSpaceId,
@ -106,6 +107,9 @@ const getSpaceHierarchy = (
const hierarchy: SpaceHierarchy[] = spaceItems.map((spaceItem) => {
const childEvents = roomToChildEvents.get(spaceItem.roomId);
if (!spaceRooms.has(spaceItem.roomId) || closedCategory(spaceItem.roomId)) {
return { space: spaceItem };
}
const childItems: HierarchyItemRoom[] = [];
childEvents?.forEach((childEvent) => {
@ -131,15 +135,19 @@ const getSpaceHierarchy = (
return hierarchy;
};
export const useSpaceHierarchy = (spaceId: string): SpaceHierarchy[] => {
export const useSpaceHierarchy = (
spaceId: string,
closedCategory: (spaceId: string) => boolean
): SpaceHierarchy[] => {
const ct = useClient();
const roomToChildEvents = useObservableValue(ct.roomToChildEvents);
const spaceRooms = useObservableValue(ct.spaceRooms);
const spaceRoomsCache = useAtomValue(spaceRoomsAtom);
const hierarchy = useMemo(
() => getSpaceHierarchy(spaceId, roomToChildEvents, spaceRooms, spaceRoomsCache),
[spaceId, roomToChildEvents, spaceRooms, spaceRoomsCache]
() =>
getSpaceHierarchy(spaceId, roomToChildEvents, spaceRooms, spaceRoomsCache, closedCategory),
[spaceId, roomToChildEvents, spaceRooms, spaceRoomsCache, closedCategory]
);
return hierarchy;
@ -216,7 +224,7 @@ const PER_PAGE_COUNT = 100;
const MAX_AUTO_PAGE_COUNT = 50;
export type FetchSpaceHierarchyLevelData = {
fetching: boolean;
error: Error | null;
error: unknown;
rooms: Map<string, SpaceHierarchyRoomChunk>;
};
export const useFetchSpaceHierarchyLevel = (

View file

@ -230,8 +230,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
element={<WelcomePage />}
/>
)}
{/* <Route path={_LOBBY_PATH} element={<Lobby />} />
<Route path={_SEARCH_PATH} element={<SpaceSearch />} /> */}
<Route path={_LOBBY_PATH} element={<Lobby />} />
{/*<Route path={_SEARCH_PATH} element={<SpaceSearch />} /> */}
<Route
path={_ROOM_PATH}
element={

View file

@ -1,6 +1,7 @@
import { ComputedObservableValue, ObservableValue } from 'neurone/lib/utils/observable';
import { MatrixRequest } from 'neurone/lib/matrix-request';
import { HistoryVisibility, Machine } from 'neurone/lib/crypto/machine';
import { ObjectReader } from 'neurone/lib/utils';
import { Disposer } from 'neurone/lib/utils/disposable';
import {
MessageEventWithoutRoomId,
@ -22,7 +23,6 @@ import { GetTxnId } from './txn';
import { Timeline } from './room';
import { AccountDataType } from '../types/matrix/accountData';
import { DecryptionManager } from './decryption';
import { ObjectReader } from 'neurone/lib/utils';
export type StateContext = {
join: {
@ -89,6 +89,7 @@ export type ClientContext = {
roomToAvatar: ObservableValue<Map<string, string>>;
roomToHistoryVisibility: ObservableValue<Map<string, HistoryVisibility>>;
roomToJoinRule: ObservableValue<Map<string, JoinRule>>;
roomToType: ObservableValue<Map<string, string | undefined>>;
roomToReplacement: ObservableValue<Map<string, string>>;
roomToCreators: ObservableValue<Map<string, Set<string>>>;
roomToVersion: ObservableValue<Map<string, string>>;
@ -389,10 +390,10 @@ export const createRoomContext = async (
};
};
export const createEncryptedRoomsObservable = async (
export const createEncryptedRoomsObservable = (
state: StateContext,
disposer: Disposer
): Promise<ObservableValue<Set<string>>> => {
): ObservableValue<Set<string>> => {
const encryptedRoomsObservable: ObservableValue<Set<string>> = new ComputedObservableValue(() => {
const encryptedRooms = new Set<string>();
@ -440,10 +441,10 @@ export const createDirectRoomsObservable = async (
return directRoomsObservable;
};
export const createRoomToReplacementObservable = async (
export const createRoomToReplacementObservable = (
state: StateContext,
disposer: Disposer
): Promise<ObservableValue<Map<string, string>>> => {
): ObservableValue<Map<string, string>> => {
const roomToReplacementObservable: ObservableValue<Map<string, string>> =
new ComputedObservableValue(() => {
const roomToReplacement = new Map<string, string>();
@ -463,13 +464,13 @@ export const createRoomToReplacementObservable = async (
return roomToReplacementObservable;
};
export const createSpaceMapping = async (
export const createSpaceMapping = (
state: StateContext,
disposer: Disposer
): Promise<{
): {
roomToParents: ObservableValue<Map<string, Set<string>>>;
roomToChildEvents: ObservableValue<Map<string, StateEventWithoutRoomId<SpaceChildContent>[]>>;
}> => {
} => {
const roomToParentsObservable: ObservableValue<Map<string, Set<string>>> =
new ComputedObservableValue(() => {
const roomToParents = new Map<string, Set<string>>();
@ -533,10 +534,10 @@ export const createRoomToSummaryObservable = async (
return roomToSummaryObservable;
};
export const createRoomToAliasObservable = async (
export const createRoomToAliasObservable = (
state: StateContext,
disposer: Disposer
): Promise<ObservableValue<Map<string, string>>> => {
): ObservableValue<Map<string, string>> => {
const roomToAliasObservable: ObservableValue<Map<string, string>> =
new ComputedObservableValue(() => {
const roomToAlias = new Map<string, string>();
@ -570,10 +571,10 @@ export const createRoomToAliasObservable = async (
return roomToAliasObservable;
};
export const createRoomToNameObservable = async (
export const createRoomToNameObservable = (
state: StateContext,
disposer: Disposer
): Promise<ObservableValue<Map<string, string>>> => {
): ObservableValue<Map<string, string>> => {
const roomToNameObservable: ObservableValue<Map<string, string>> =
new ComputedObservableValue(() => {
const roomToName = new Map<string, string>();
@ -601,10 +602,10 @@ export const createRoomToNameObservable = async (
return roomToNameObservable;
};
export const createRoomToAvatarObservable = async (
export const createRoomToAvatarObservable = (
state: StateContext,
disposer: Disposer
): Promise<ObservableValue<Map<string, string>>> => {
): ObservableValue<Map<string, string>> => {
const roomToAvatarObservable: ObservableValue<Map<string, string>> =
new ComputedObservableValue(() => {
const roomToAvatar = new Map<string, string>();
@ -631,10 +632,10 @@ export const createRoomToAvatarObservable = async (
return roomToAvatarObservable;
};
export const createRoomToHistoryVisibilityObservable = async (
export const createRoomToHistoryVisibilityObservable = (
state: StateContext,
disposer: Disposer
): Promise<ObservableValue<Map<string, HistoryVisibility>>> => {
): ObservableValue<Map<string, HistoryVisibility>> => {
const roomToHistoryVisibilityObservable: ObservableValue<Map<string, HistoryVisibility>> =
new ComputedObservableValue(() => {
const roomToHistoryVisibility = new Map<string, HistoryVisibility>();
@ -657,10 +658,10 @@ export const createRoomToHistoryVisibilityObservable = async (
return roomToHistoryVisibilityObservable;
};
export const createRoomToJoinRuleObservable = async (
export const createRoomToJoinRuleObservable = (
state: StateContext,
disposer: Disposer
): Promise<ObservableValue<Map<string, JoinRule>>> => {
): ObservableValue<Map<string, JoinRule>> => {
const roomToJoinRuleObservable: ObservableValue<Map<string, JoinRule>> =
new ComputedObservableValue(() => {
const roomToJoinRule = new Map<string, JoinRule>();
@ -689,6 +690,32 @@ export const createRoomToJoinRuleObservable = async (
return roomToJoinRuleObservable;
};
export const createRoomToTypeObservable = (
state: StateContext,
disposer: Disposer
): ObservableValue<Map<string, string | undefined>> => {
const roomToTypeObservable: ObservableValue<Map<string, string | undefined>> =
new ComputedObservableValue(() => {
const roomToJoinRule = new Map<string, string | undefined>();
const addType = (obj: RoomStateObject) => {
const { content } = obj.event;
const type = ObjectReader.string(content, 'type');
roomToJoinRule.set(obj.roomId, type);
};
state.join.create.read.forEach(addType);
state.invite.create.read.forEach(addType);
state.knock.create.read.forEach(addType);
state.leave.create.read.forEach(addType);
return roomToJoinRule;
}, [state.join.create, state.invite.create, state.knock.create, state.leave.create]);
disposer.collect(roomToTypeObservable);
return roomToTypeObservable;
};
export const createRoomToLatestEventObservable = async (
storage: Storage,
disposer: Disposer

View file

@ -19,6 +19,7 @@ import {
createRoomToNameObservable,
createRoomToReplacementObservable,
createRoomToSummaryObservable,
createRoomToTypeObservable,
createSpaceMapping,
createStateContext,
createSyncTokenObservable,
@ -81,17 +82,18 @@ export const initializeClient = async (session: Session): Promise<ClientContext>
state,
disposer
);
const encryptedRooms = await createEncryptedRoomsObservable(state, disposer);
const encryptedRooms = createEncryptedRoomsObservable(state, disposer);
const directRooms = await createDirectRoomsObservable(storage, disposer);
const { roomToParents, roomToChildEvents } = await createSpaceMapping(state, disposer);
const { roomToParents, roomToChildEvents } = createSpaceMapping(state, disposer);
const roomToSummary = await createRoomToSummaryObservable(storage, disposer);
const roomToAlias = await createRoomToAliasObservable(state, disposer);
const roomToName = await createRoomToNameObservable(state, disposer);
const roomToAvatar = await createRoomToAvatarObservable(state, disposer);
const roomToHistoryVisibility = await createRoomToHistoryVisibilityObservable(state, disposer);
const roomToJoinRule = await createRoomToJoinRuleObservable(state, disposer);
const roomToReplacement = await createRoomToReplacementObservable(state, disposer);
const roomToAlias = createRoomToAliasObservable(state, disposer);
const roomToName = createRoomToNameObservable(state, disposer);
const roomToAvatar = createRoomToAvatarObservable(state, disposer);
const roomToHistoryVisibility = createRoomToHistoryVisibilityObservable(state, disposer);
const roomToJoinRule = createRoomToJoinRuleObservable(state, disposer);
const roomToType = createRoomToTypeObservable(state, disposer);
const roomToReplacement = createRoomToReplacementObservable(state, disposer);
const roomToLatestEvent = await createRoomToLatestEventObservable(storage, disposer);
const syncTokenObservable = await createSyncTokenObservable(storage, disposer);
@ -137,6 +139,7 @@ export const initializeClient = async (session: Session): Promise<ClientContext>
roomToAvatar,
roomToHistoryVisibility,
roomToJoinRule,
roomToType,
roomToReplacement,
roomToLatestEvent,
roomToCreators,

View file

@ -1,15 +1,15 @@
import { ObjectReader } from 'neurone/lib/utils';
export class rError {
static error(data?: object): string | undefined {
static error(data?: unknown): string | undefined {
return ObjectReader.string(data, 'error');
}
static errcode(data?: object): string | undefined {
static errcode(data?: unknown): string | undefined {
return ObjectReader.string(data, 'errcode');
}
static retryAfterMs(data?: object): number | undefined {
static retryAfterMs(data?: unknown): number | undefined {
return ObjectReader.number(data, 'retry_after_ms');
}
}

View file

@ -92,9 +92,11 @@ export type JoinRule =
| 'restricted'
| 'knock_restricted';
export type AllowConditionType = 'm.room_membership';
export type AllowCondition = {
room_id: string;
type: string[];
type: AllowConditionType;
};
export type JoinRulesContent = {
allow?: AllowCondition[];