Compare commits

..

6 Commits

Author SHA1 Message Date
Ajay Bura
3ed8260877 Release v4.8.1 (#2360) 2025-06-10 23:48:55 +10:00
Ajay Bura
44347db6e4 Add allow from currently selected space if no m.space.parent found (#2359) 2025-06-10 23:47:46 +10:00
Ajay Bura
91632aa193 Fix space navigation & view space timeline dev-option (#2358)
* fix inaccessible space on alias change

* fix new room in space open in home

* allow opening space timeline

* hide event timeline feature behind dev tool

* add navToActivePath to clear cache function
2025-06-10 14:44:17 +10:00
Ajay Bura
e6f4eeca8e Update folds to v2.2.0 (#2341) 2025-05-27 14:10:27 +05:30
Ajay Bura
a23279e633 Fix rate limit when reordering in space lobby (#2254)
* move can drop lobby item logic to hook

* add comment

* resolve rate limit when reordering space children
2025-05-26 14:21:27 +05:30
Krishan
83057ebbd4 Fix additional spam string matching (#2339) 2025-05-25 15:51:19 +05:30
20 changed files with 401 additions and 231 deletions

23
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.8.0", "version": "4.8.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cinny", "name": "cinny",
"version": "4.8.0", "version": "4.8.1",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "1.1.6", "@atlaskit/pragmatic-drag-and-drop": "1.1.6",
@@ -34,7 +34,7 @@
"file-saver": "2.0.5", "file-saver": "2.0.5",
"flux": "4.0.3", "flux": "4.0.3",
"focus-trap-react": "10.0.2", "focus-trap-react": "10.0.2",
"folds": "2.1.0", "folds": "2.2.0",
"formik": "2.4.6", "formik": "2.4.6",
"html-dom-parser": "4.0.0", "html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "4.2.0",
@@ -7265,15 +7265,16 @@
} }
}, },
"node_modules/folds": { "node_modules/folds": {
"version": "2.1.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/folds/-/folds-2.1.0.tgz", "resolved": "https://registry.npmjs.org/folds/-/folds-2.2.0.tgz",
"integrity": "sha512-KwAG8bH3jsyZ9FKPMg+6ABV2YOcpp4nL0cCelsalnaPeRThkc5fgG1Xj5mhmdffYKjEXpEbERi5qmGbepgJryg==", "integrity": "sha512-uOfck5eWEIK11rhOAEdSoPIvMXwv+D1Go03pxSAKezWVb+uRoBdmE6LEqiLOF+ac4DGmZRMPvpdDsXCg7EVNIg==",
"license": "Apache-2.0",
"peerDependencies": { "peerDependencies": {
"@vanilla-extract/css": "^1.9.2", "@vanilla-extract/css": "1.9.2",
"@vanilla-extract/recipes": "^0.3.0", "@vanilla-extract/recipes": "0.3.0",
"classnames": "^2.3.2", "classnames": "2.3.2",
"react": "^17.0.0", "react": "17.0.0",
"react-dom": "^17.0.0" "react-dom": "17.0.0"
} }
}, },
"node_modules/for-each": { "node_modules/for-each": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "cinny", "name": "cinny",
"version": "4.8.0", "version": "4.8.1",
"description": "Yet another matrix client", "description": "Yet another matrix client",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -45,7 +45,7 @@
"file-saver": "2.0.5", "file-saver": "2.0.5",
"flux": "4.0.3", "flux": "4.0.3",
"focus-trap-react": "10.0.2", "focus-trap-react": "10.0.2",
"folds": "2.1.0", "folds": "2.2.0",
"formik": "2.4.6", "formik": "2.4.6",
"html-dom-parser": "4.0.0", "html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "4.2.0",

View File

@@ -2,6 +2,7 @@ import React, { useCallback, useMemo } from 'react';
import { color, Text } from 'folds'; import { color, Text } from 'folds';
import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk'; import { JoinRule, MatrixError, RestrictedAllowType } from 'matrix-js-sdk';
import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types'; import { RoomJoinRulesEventContent } from 'matrix-js-sdk/lib/types';
import { useAtomValue } from 'jotai';
import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels'; import { IPowerLevels, powerLevelAPI } from '../../../hooks/usePowerLevels';
import { import {
ExtendedJoinRules, ExtendedJoinRules,
@@ -20,6 +21,12 @@ import { useStateEvent } from '../../../hooks/useStateEvent';
import { useSpaceOptionally } from '../../../hooks/useSpace'; import { useSpaceOptionally } from '../../../hooks/useSpace';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { getStateEvents } from '../../../utils/room'; import { getStateEvents } from '../../../utils/room';
import {
useRecursiveChildSpaceScopeFactory,
useSpaceChildren,
} from '../../../state/hooks/roomList';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
type RestrictedRoomAllowContent = { type RestrictedRoomAllowContent = {
room_id: string; room_id: string;
@@ -36,7 +43,11 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
const allowKnockRestricted = roomVersion >= 10; const allowKnockRestricted = roomVersion >= 10;
const allowRestricted = roomVersion >= 8; const allowRestricted = roomVersion >= 8;
const allowKnock = roomVersion >= 7; const allowKnock = roomVersion >= 7;
const roomIdToParents = useAtomValue(roomToParentsAtom);
const space = useSpaceOptionally(); const space = useSpaceOptionally();
const subspacesScope = useRecursiveChildSpaceScopeFactory(mx, roomIdToParents);
const subspaces = useSpaceChildren(allRoomsAtom, space?.roomId ?? '', subspacesScope);
const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId()); const userPowerLevel = powerLevelAPI.getPowerLevel(powerLevels, mx.getSafeUserId());
const canEdit = powerLevelAPI.canSendStateEvent( const canEdit = powerLevelAPI.canSendStateEvent(
@@ -74,9 +85,22 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
async (joinRule: ExtendedJoinRules) => { async (joinRule: ExtendedJoinRules) => {
const allow: RestrictedRoomAllowContent[] = []; const allow: RestrictedRoomAllowContent[] = [];
if (joinRule === JoinRule.Restricted || joinRule === 'knock_restricted') { if (joinRule === JoinRule.Restricted || joinRule === 'knock_restricted') {
const parents = getStateEvents(room, StateEvent.SpaceParent).map((event) => const roomParents = roomIdToParents.get(room.roomId);
event.getStateKey()
); const parents = getStateEvents(room, StateEvent.SpaceParent)
.map((event) => event.getStateKey())
.filter((parentId) => typeof parentId === 'string')
.filter((parentId) => roomParents?.has(parentId));
if (parents.length === 0 && space && 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);
}
selectedParents.forEach((pId) => parents.push(pId));
}
parents.forEach((parentRoomId) => { parents.forEach((parentRoomId) => {
if (!parentRoomId) return; if (!parentRoomId) return;
allow.push({ allow.push({
@@ -92,7 +116,7 @@ export function RoomJoinRules({ powerLevels }: RoomJoinRulesProps) {
if (allow.length > 0) c.allow = allow; if (allow.length > 0) c.allow = allow;
await mx.sendStateEvent(room.roomId, StateEvent.RoomJoinRules as any, c); await mx.sendStateEvent(room.roomId, StateEvent.RoomJoinRules as any, c);
}, },
[mx, room] [mx, room, space, subspaces, roomIdToParents]
) )
); );

View File

@@ -1,5 +1,5 @@
import React, { MouseEventHandler, useCallback, useMemo, useRef, useState } from 'react'; import React, { MouseEventHandler, useCallback, useMemo, useRef, useState } from 'react';
import { Box, Icon, IconButton, Icons, Line, Scroll, config } from 'folds'; import { Box, Chip, Icon, IconButton, Icons, Line, Scroll, Spinner, Text, config } from 'folds';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -36,7 +36,7 @@ import { makeLobbyCategoryId } from '../../state/closedLobbyCategories';
import { useCategoryHandler } from '../../hooks/useCategoryHandler'; import { useCategoryHandler } from '../../hooks/useCategoryHandler';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { allRoomsAtom } from '../../state/room-list/roomList'; import { allRoomsAtom } from '../../state/room-list/roomList';
import { getCanonicalAliasOrRoomId } from '../../utils/matrix'; import { getCanonicalAliasOrRoomId, rateLimitedActions } from '../../utils/matrix';
import { getSpaceRoomPath } from '../../pages/pathUtils'; import { getSpaceRoomPath } from '../../pages/pathUtils';
import { StateEvent } from '../../../types/matrix/room'; import { StateEvent } from '../../../types/matrix/room';
import { CanDropCallback, useDnDMonitor } from './DnD'; import { CanDropCallback, useDnDMonitor } from './DnD';
@@ -53,6 +53,95 @@ import { roomToParentsAtom } from '../../state/room/roomToParents';
import { AccountDataEvent } from '../../../types/matrix/accountData'; import { AccountDataEvent } from '../../../types/matrix/accountData';
import { useRoomMembers } from '../../hooks/useRoomMembers'; import { useRoomMembers } from '../../hooks/useRoomMembers';
import { SpaceHierarchy } from './SpaceHierarchy'; import { SpaceHierarchy } from './SpaceHierarchy';
import { useGetRoom } from '../../hooks/useGetRoom';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
const useCanDropLobbyItem = (
space: Room,
roomsPowerLevels: Map<string, IPowerLevels>,
getRoom: (roomId: string) => Room | undefined,
canEditSpaceChild: (powerLevels: IPowerLevels) => boolean
): CanDropCallback => {
const mx = useMatrixClient();
const canDropSpace: CanDropCallback = useCallback(
(item, container) => {
if (!('space' in container.item)) {
// can not drop around rooms.
// space can only be drop around other spaces
return false;
}
const containerSpaceId = space.roomId;
if (
getRoom(containerSpaceId) === undefined ||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
) {
return false;
}
return true;
},
[space, roomsPowerLevels, getRoom, canEditSpaceChild]
);
const canDropRoom: CanDropCallback = useCallback(
(item, container) => {
const containerSpaceId =
'space' in container.item ? container.item.roomId : container.item.parentId;
const draggingOutsideSpace = item.parentId !== containerSpaceId;
const restrictedItem = mx.getRoom(item.roomId)?.getJoinRule() === JoinRule.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 itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
const userPLInItem = powerLevelAPI.getPowerLevel(
itemPowerLevel,
mx.getUserId() ?? undefined
);
const canChangeJoinRuleAllow = powerLevelAPI.canSendStateEvent(
itemPowerLevel,
StateEvent.RoomJoinRules,
userPLInItem
);
if (!canChangeJoinRuleAllow) {
return false;
}
}
if (
getRoom(containerSpaceId) === undefined ||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
) {
return false;
}
return true;
},
[mx, getRoom, canEditSpaceChild, roomsPowerLevels]
);
const canDrop: CanDropCallback = useCallback(
(item, container): boolean => {
if (item.roomId === container.item.roomId || item.roomId === container.nextRoomId) {
// can not drop before or after itself
return false;
}
// if we are dragging a space
if ('space' in item) {
return canDropSpace(item, container);
}
return canDropRoom(item, container);
},
[canDropSpace, canDropRoom]
);
return canDrop;
};
export function Lobby() { export function Lobby() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -92,15 +181,7 @@ export function Lobby() {
useCallback((w, height) => setHeroSectionHeight(height), []) useCallback((w, height) => setHeroSectionHeight(height), [])
); );
const getRoom = useCallback( const getRoom = useGetRoom(allJoinedRooms);
(rId: string) => {
if (allJoinedRooms.has(rId)) {
return mx.getRoom(rId) ?? undefined;
}
return undefined;
},
[mx, allJoinedRooms]
);
const canEditSpaceChild = useCallback( const canEditSpaceChild = useCallback(
(powerLevels: IPowerLevels) => (powerLevels: IPowerLevels) =>
@@ -150,64 +231,16 @@ export function Lobby() {
) )
); );
const canDrop: CanDropCallback = useCallback( const canDrop: CanDropCallback = useCanDropLobbyItem(
(item, container): boolean => { space,
const restrictedItem = mx.getRoom(item.roomId)?.getJoinRule() === JoinRule.Restricted; roomsPowerLevels,
if (item.roomId === container.item.roomId || item.roomId === container.nextRoomId) { getRoom,
// can not drop before or after itself canEditSpaceChild
return false;
}
if ('space' in item) {
if (!('space' in container.item)) return false;
const containerSpaceId = space.roomId;
if (
getRoom(containerSpaceId) === undefined ||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
) {
return false;
}
return true;
}
const containerSpaceId =
'space' in container.item ? container.item.roomId : container.item.parentId;
const dropOutsideSpace = item.parentId !== containerSpaceId;
if (dropOutsideSpace && restrictedItem) {
// do not allow restricted room to drop outside
// current space if can't change join rule allow
const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
const userPLInItem = powerLevelAPI.getPowerLevel(
itemPowerLevel,
mx.getUserId() ?? undefined
);
const canChangeJoinRuleAllow = powerLevelAPI.canSendStateEvent(
itemPowerLevel,
StateEvent.RoomJoinRules,
userPLInItem
);
if (!canChangeJoinRuleAllow) {
return false;
}
}
if (
getRoom(containerSpaceId) === undefined ||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
) {
return false;
}
return true;
},
[getRoom, space.roomId, roomsPowerLevels, canEditSpaceChild, mx]
); );
const reorderSpace = useCallback( const [reorderSpaceState, reorderSpace] = useAsyncCallback(
(item: HierarchyItemSpace, containerItem: HierarchyItem) => { useCallback(
async (item: HierarchyItemSpace, containerItem: HierarchyItem) => {
if (!item.parentId) return; if (!item.parentId) return;
const itemSpaces: HierarchyItemSpace[] = hierarchy const itemSpaces: HierarchyItemSpace[] = hierarchy
@@ -231,26 +264,38 @@ export function Lobby() {
const newOrders = orderKeys(lex, currentOrders); const newOrders = orderKeys(lex, currentOrders);
newOrders?.forEach((orderKey, index) => { const reorders = newOrders
const itm = itemSpaces[index]; ?.map((orderKey, index) => ({
if (!itm || !itm.parentId) return; item: itemSpaces[index],
const parentPL = roomsPowerLevels.get(itm.parentId); orderKey,
}))
.filter((reorder, index) => {
if (!reorder.item.parentId) return false;
const parentPL = roomsPowerLevels.get(reorder.item.parentId);
const canEdit = parentPL && canEditSpaceChild(parentPL); const canEdit = parentPL && canEditSpaceChild(parentPL);
if (canEdit && orderKey !== currentOrders[index]) { return canEdit && reorder.orderKey !== currentOrders[index];
mx.sendStateEvent(
itm.parentId,
StateEvent.SpaceChild as any,
{ ...itm.content, order: orderKey },
itm.roomId
);
}
}); });
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
);
});
}
}, },
[mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild] [mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild]
)
); );
const reorderingSpace = reorderSpaceState.status === AsyncStatus.Loading;
const reorderRoom = useCallback( const [reorderRoomState, reorderRoom] = useAsyncCallback(
(item: HierarchyItem, containerItem: HierarchyItem): void => { useCallback(
async (item: HierarchyItem, containerItem: HierarchyItem) => {
const itemRoom = mx.getRoom(item.roomId); const itemRoom = mx.getRoom(item.roomId);
if (!item.parentId) { if (!item.parentId) {
return; return;
@@ -259,6 +304,7 @@ export function Lobby() {
'space' in containerItem ? containerItem.roomId : containerItem.parentId; 'space' in containerItem ? containerItem.roomId : containerItem.parentId;
const itemContent = item.content; const itemContent = item.content;
// remove from current space
if (item.parentId !== containerParentId) { if (item.parentId !== containerParentId) {
mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId); mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId);
} }
@@ -277,7 +323,8 @@ export function Lobby() {
if (joinRuleContent) { if (joinRuleContent) {
const allow = const allow =
joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ?? []; joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ??
[];
allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId }); allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, { mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, {
...joinRuleContent, ...joinRuleContent,
@@ -310,20 +357,29 @@ export function Lobby() {
const newOrders = orderKeys(lex, currentOrders); const newOrders = orderKeys(lex, currentOrders);
newOrders?.forEach((orderKey, index) => { const reorders = newOrders
const itm = itemSpaces[index]; ?.map((orderKey, index) => ({
if (itm && orderKey !== currentOrders[index]) { item: itemSpaces[index],
mx.sendStateEvent( orderKey,
}))
.filter((reorder, index) => reorder.item && reorder.orderKey !== currentOrders[index]);
if (reorders) {
await rateLimitedActions(reorders, async (reorder) => {
await mx.sendStateEvent(
containerParentId, containerParentId,
StateEvent.SpaceChild as any, StateEvent.SpaceChild as any,
{ ...itm.content, order: orderKey }, { ...reorder.item.content, order: reorder.orderKey },
itm.roomId reorder.item.roomId
); );
}
}); });
}
}, },
[mx, hierarchy, lex] [mx, hierarchy, lex]
)
); );
const reorderingRoom = reorderRoomState.status === AsyncStatus.Loading;
const reordering = reorderingRoom || reorderingSpace;
useDnDMonitor( useDnDMonitor(
scrollRef, scrollRef,
@@ -449,6 +505,7 @@ export function Lobby() {
draggingItem={draggingItem} draggingItem={draggingItem}
onDragging={setDraggingItem} onDragging={setDraggingItem}
canDrop={canDrop} canDrop={canDrop}
disabledReorder={reordering}
nextSpaceId={nextSpaceId} nextSpaceId={nextSpaceId}
getRoom={getRoom} getRoom={getRoom}
pinned={sidebarSpaces.has(item.space.roomId)} pinned={sidebarSpaces.has(item.space.roomId)}
@@ -460,6 +517,28 @@ export function Lobby() {
); );
})} })}
</div> </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> </PageContentCenter>
</PageContent> </PageContent>
</Scroll> </Scroll>

View File

@@ -31,6 +31,7 @@ type SpaceHierarchyProps = {
draggingItem?: HierarchyItem; draggingItem?: HierarchyItem;
onDragging: (item?: HierarchyItem) => void; onDragging: (item?: HierarchyItem) => void;
canDrop: CanDropCallback; canDrop: CanDropCallback;
disabledReorder?: boolean;
nextSpaceId?: string; nextSpaceId?: string;
getRoom: (roomId: string) => Room | undefined; getRoom: (roomId: string) => Room | undefined;
pinned: boolean; pinned: boolean;
@@ -54,6 +55,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
draggingItem, draggingItem,
onDragging, onDragging,
canDrop, canDrop,
disabledReorder,
nextSpaceId, nextSpaceId,
getRoom, getRoom,
pinned, pinned,
@@ -116,7 +118,9 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
handleClose={handleClose} handleClose={handleClose}
getRoom={getRoom} getRoom={getRoom}
canEditChild={canEditSpaceChild(spacePowerLevels)} canEditChild={canEditSpaceChild(spacePowerLevels)}
canReorder={parentPowerLevels ? canEditSpaceChild(parentPowerLevels) : false} canReorder={
parentPowerLevels && !disabledReorder ? canEditSpaceChild(parentPowerLevels) : false
}
options={ options={
parentId && parentId &&
parentPowerLevels && ( parentPowerLevels && (
@@ -174,7 +178,7 @@ export const SpaceHierarchy = forwardRef<HTMLDivElement, SpaceHierarchyProps>(
dm={mDirects.has(roomItem.roomId)} dm={mDirects.has(roomItem.roomId)}
onOpen={onOpenRoom} onOpen={onOpenRoom}
getRoom={getRoom} getRoom={getRoom}
canReorder={canEditSpaceChild(spacePowerLevels)} canReorder={canEditSpaceChild(spacePowerLevels) && !disabledReorder}
options={ options={
<HierarchyItemMenu <HierarchyItemMenu
item={roomItem} item={roomItem}

View File

@@ -71,7 +71,7 @@ const useSettingsMenuItems = (): SettingsMenuItem[] =>
{ {
page: SettingsPages.DevicesPage, page: SettingsPages.DevicesPage,
name: 'Devices', name: 'Devices',
icon: Icons.Category, icon: Icons.Monitor,
}, },
{ {
page: SettingsPages.EmojisStickersPage, page: SettingsPages.EmojisStickersPage,

View File

@@ -13,6 +13,8 @@ import { getOrphanParents } from '../utils/room';
import { roomToParentsAtom } from '../state/room/roomToParents'; import { roomToParentsAtom } from '../state/room/roomToParents';
import { mDirectAtom } from '../state/mDirectList'; import { mDirectAtom } from '../state/mDirectList';
import { useSelectedSpace } from './router/useSelectedSpace'; import { useSelectedSpace } from './router/useSelectedSpace';
import { settingsAtom } from '../state/settings';
import { useSetting } from '../state/hooks/settings';
export const useRoomNavigate = () => { export const useRoomNavigate = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -20,6 +22,7 @@ export const useRoomNavigate = () => {
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const spaceSelectedId = useSelectedSpace(); const spaceSelectedId = useSelectedSpace();
const [developerTools] = useSetting(settingsAtom, 'developerTools');
const navigateSpace = useCallback( const navigateSpace = useCallback(
(roomId: string) => { (roomId: string) => {
@@ -32,15 +35,22 @@ export const useRoomNavigate = () => {
const navigateRoom = useCallback( const navigateRoom = useCallback(
(roomId: string, eventId?: string, opts?: NavigateOptions) => { (roomId: string, eventId?: string, opts?: NavigateOptions) => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId); const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
const openSpaceTimeline = developerTools && spaceSelectedId === roomId;
const orphanParents = getOrphanParents(roomToParents, roomId); const orphanParents = openSpaceTimeline ? [roomId] : getOrphanParents(roomToParents, roomId);
if (orphanParents.length > 0) { if (orphanParents.length > 0) {
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId( const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(
mx, mx,
spaceSelectedId && orphanParents.includes(spaceSelectedId) spaceSelectedId && orphanParents.includes(spaceSelectedId)
? spaceSelectedId ? spaceSelectedId
: orphanParents[0] : orphanParents[0] // TODO: better orphan parent selection.
); );
if (openSpaceTimeline) {
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomId, eventId), opts);
return;
}
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts); navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId), opts);
return; return;
} }
@@ -52,7 +62,7 @@ export const useRoomNavigate = () => {
navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts); navigate(getHomeRoomPath(roomIdOrAlias, eventId), opts);
}, },
[mx, navigate, spaceSelectedId, roomToParents, mDirects] [mx, navigate, spaceSelectedId, roomToParents, mDirects, developerTools]
); );
return { return {

View File

@@ -28,9 +28,11 @@ import { allRoomsAtom } from '../../state/room-list/roomList';
import { mDirectAtom } from '../../state/mDirectList'; import { mDirectAtom } from '../../state/mDirectList';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getViaServers } from '../../plugins/via-servers'; import { getViaServers } from '../../plugins/via-servers';
import { rateLimitedActions } from '../../utils/matrix';
import { useAlive } from '../../hooks/useAlive';
function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) { function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
const mountStore = useStore(roomId); const alive = useAlive();
const [debounce] = useState(new Debounce()); const [debounce] = useState(new Debounce());
const [process, setProcess] = useState(null); const [process, setProcess] = useState(null);
const [allRoomIds, setAllRoomIds] = useState([]); const [allRoomIds, setAllRoomIds] = useState([]);
@@ -68,14 +70,14 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
const handleAdd = async () => { const handleAdd = async () => {
setProcess(`Adding ${selected.length} items...`); setProcess(`Adding ${selected.length} items...`);
const promises = selected.map((rId) => { await rateLimitedActions(selected, async (rId) => {
const room = mx.getRoom(rId); const room = mx.getRoom(rId);
const via = getViaServers(room); const via = getViaServers(room);
if (via.length === 0) { if (via.length === 0) {
via.push(getIdServer(rId)); via.push(getIdServer(rId));
} }
return mx.sendStateEvent( await mx.sendStateEvent(
roomId, roomId,
'm.space.child', 'm.space.child',
{ {
@@ -87,9 +89,7 @@ function SpaceAddExistingContent({ roomId, spaces: onlySpaces }) {
); );
}); });
mountStore.setItem(true); if (!alive()) return;
await Promise.allSettled(promises);
if (mountStore.getItem() !== true) return;
const roomIds = onlySpaces ? [...spaces] : [...rooms, ...directs]; const roomIds = onlySpaces ? [...spaces] : [...rooms, ...directs];
const allIds = roomIds.filter( const allIds = roomIds.filter(

View File

@@ -15,7 +15,7 @@ export function AuthFooter() {
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
v4.8.0 v4.8.1
</Text> </Text>
<Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer"> <Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
Twitter Twitter

View File

@@ -24,7 +24,7 @@ export function WelcomePage() {
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
v4.8.0 v4.8.1
</a> </a>
</span> </span>
} }

View File

@@ -209,7 +209,7 @@ export function Explore() {
<Box as="span" grow="Yes" alignItems="Center" gap="200"> <Box as="span" grow="Yes" alignItems="Center" gap="200">
<Avatar size="200" radii="400"> <Avatar size="200" radii="400">
<Icon <Icon
src={Icons.Category} src={Icons.Server}
size="100" size="100"
filled={selectedServer === userServer} filled={selectedServer === userServer}
/> />
@@ -243,11 +243,7 @@ export function Explore() {
<NavItemContent> <NavItemContent>
<Box as="span" grow="Yes" alignItems="Center" gap="200"> <Box as="span" grow="Yes" alignItems="Center" gap="200">
<Avatar size="200" radii="400"> <Avatar size="200" radii="400">
<Icon <Icon src={Icons.Server} size="100" filled={server === selectedServer} />
src={Icons.Category}
size="100"
filled={server === selectedServer}
/>
</Avatar> </Avatar>
<Box as="span" grow="Yes"> <Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate> <Text as="span" size="Inherit" truncate>

View File

@@ -507,7 +507,7 @@ export function PublicRooms() {
)} )}
</Box> </Box>
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Category} />} {screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Server} />}
<Text size="H3" truncate> <Text size="H3" truncate>
{server} {server}
</Text> </Text>

View File

@@ -744,13 +744,14 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
const targetSpaceId = target.getAttribute('data-id'); const targetSpaceId = target.getAttribute('data-id');
if (!targetSpaceId) return; if (!targetSpaceId) return;
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId));
if (screenSize === ScreenSize.Mobile) { if (screenSize === ScreenSize.Mobile) {
navigate(getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId))); navigate(spacePath);
return; return;
} }
const activePath = navToActivePath.get(targetSpaceId); const activePath = navToActivePath.get(targetSpaceId);
if (activePath) { if (activePath && activePath.pathname.startsWith(spacePath)) {
navigate(joinPathComponent(activePath)); navigate(joinPathComponent(activePath));
return; return;
} }

View File

@@ -1,21 +1,24 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom'; import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate'; import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
import { useSpace } from '../../../hooks/useSpace'; import { useSpace } from '../../../hooks/useSpace';
import { getAllParents } from '../../../utils/room'; import { getAllParents, getSpaceChildren } from '../../../utils/room';
import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers'; import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
import { mDirectAtom } from '../../../state/mDirectList'; import { mDirectAtom } from '../../../state/mDirectList';
import { settingsAtom } from '../../../state/settings';
import { useSetting } from '../../../state/hooks/settings';
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) { export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const space = useSpace(); const space = useSpace();
const roomToParents = useAtomValue(roomToParentsAtom); const [developerTools] = useSetting(settingsAtom, 'developerTools');
const [roomToParents, setRoomToParents] = useAtom(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const allRooms = useAtomValue(allRoomsAtom); const allRooms = useAtomValue(allRoomsAtom);
@@ -24,12 +27,36 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const roomId = useSelectedRoom(); const roomId = useSelectedRoom();
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
if ( if (!room || !allRooms.includes(room.roomId)) {
!room || // room is not joined
room.isSpaceRoom() || return (
!allRooms.includes(room.roomId) || <JoinBeforeNavigate
!getAllParents(roomToParents, room.roomId).has(space.roomId) roomIdOrAlias={roomIdOrAlias!}
) { eventId={eventId}
viaServers={viaServers}
/>
);
}
if (developerTools && room.isSpaceRoom() && room.roomId === space.roomId) {
// allow to view space timeline
return (
<RoomProvider key={room.roomId} value={room}>
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>{children}</IsDirectRoomProvider>
</RoomProvider>
);
}
if (!getAllParents(roomToParents, room.roomId).has(space.roomId)) {
if (getSpaceChildren(space).includes(room.roomId)) {
// fill missing roomToParent mapping
setRoomToParents({
type: 'PUT',
parent: space.roomId,
children: [room.roomId],
});
}
return ( return (
<JoinBeforeNavigate <JoinBeforeNavigate
roomIdOrAlias={roomIdOrAlias!} roomIdOrAlias={roomIdOrAlias!}

View File

@@ -75,6 +75,7 @@ import {
useRoomsNotificationPreferencesContext, useRoomsNotificationPreferencesContext,
} from '../../../hooks/useRoomsNotificationPreferences'; } from '../../../hooks/useRoomsNotificationPreferences';
import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings'; import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
type SpaceMenuProps = { type SpaceMenuProps = {
room: Room; room: Room;
@@ -83,11 +84,13 @@ type SpaceMenuProps = {
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => { const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClose }, ref) => {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [developerTools] = useSetting(settingsAtom, 'developerTools');
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const powerLevels = usePowerLevels(room); const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels); const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? '')); const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const openSpaceSettings = useOpenSpaceSettings(); const openSpaceSettings = useOpenSpaceSettings();
const { navigateRoom } = useRoomNavigate();
const allChild = useSpaceChildren( const allChild = useSpaceChildren(
allRoomsAtom, allRoomsAtom,
@@ -118,6 +121,11 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
requestClose(); requestClose();
}; };
const handleOpenTimeline = () => {
navigateRoom(room.roomId);
requestClose();
};
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
@@ -168,6 +176,18 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(({ room, requestClo
Space Settings Space Settings
</Text> </Text>
</MenuItem> </MenuItem>
{developerTools && (
<MenuItem
onClick={handleOpenTimeline}
size="300"
after={<Icon size="100" src={Icons.Terminal} />}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Event Timeline
</Text>
</MenuItem>
)}
</Box> </Box>
<Line variant="Surface" size="300" /> <Line variant="Surface" size="300" />
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>

View File

@@ -1,7 +1,7 @@
import * as badWords from 'badwords-list'; import * as badWords from 'badwords-list';
import { sanitizeForRegex } from '../utils/regex'; import { sanitizeForRegex } from '../utils/regex';
const additionalBadWords: string[] = ['Torture', 'T0rture']; const additionalBadWords: string[] = ['torture', 't0rture'];
const fullBadWordList = additionalBadWords.concat( const fullBadWordList = additionalBadWords.concat(
badWords.array.filter((word) => !additionalBadWords.includes(word)) badWords.array.filter((word) => !additionalBadWords.includes(word))

View File

@@ -9,6 +9,8 @@ import {
const NAV_TO_ACTIVE_PATH = 'navToActivePath'; const NAV_TO_ACTIVE_PATH = 'navToActivePath';
const getStoreKey = (userId: string): string => `${NAV_TO_ACTIVE_PATH}${userId}`;
type NavToActivePath = Map<string, Path>; type NavToActivePath = Map<string, Path>;
type NavToActivePathAction = type NavToActivePathAction =
@@ -25,7 +27,7 @@ type NavToActivePathAction =
export type NavToActivePathAtom = WritableAtom<NavToActivePath, [NavToActivePathAction], undefined>; export type NavToActivePathAtom = WritableAtom<NavToActivePath, [NavToActivePathAction], undefined>;
export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom => { export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom => {
const storeKey = `${NAV_TO_ACTIVE_PATH}${userId}`; const storeKey = getStoreKey(userId);
const baseNavToActivePathAtom = atomWithLocalStorage<NavToActivePath>( const baseNavToActivePathAtom = atomWithLocalStorage<NavToActivePath>(
storeKey, storeKey,
@@ -64,3 +66,7 @@ export const makeNavToActivePathAtom = (userId: string): NavToActivePathAtom =>
return navToActivePathAtom; return navToActivePathAtom;
}; };
export const clearNavToActivePathStore = (userId: string) => {
localStorage.removeItem(getStoreKey(userId));
};

View File

@@ -300,7 +300,7 @@ export const downloadEncryptedMedia = async (
export const rateLimitedActions = async <T, R = void>( export const rateLimitedActions = async <T, R = void>(
data: T[], data: T[],
callback: (item: T) => Promise<R>, callback: (item: T, index: number) => Promise<R>,
maxRetryCount?: number maxRetryCount?: number
) => { ) => {
let retryCount = 0; let retryCount = 0;
@@ -312,8 +312,8 @@ export const rateLimitedActions = async <T, R = void>(
setTimeout(resolve, ms); setTimeout(resolve, ms);
}); });
const performAction = async (dataItem: T) => { const performAction = async (dataItem: T, index: number) => {
const [err] = await to<R, MatrixError>(callback(dataItem)); const [err] = await to<R, MatrixError>(callback(dataItem, index));
if (err?.httpStatus === 429) { if (err?.httpStatus === 429) {
if (retryCount === maxRetryCount) { if (retryCount === maxRetryCount) {
@@ -321,11 +321,11 @@ export const rateLimitedActions = async <T, R = void>(
} }
const waitMS = err.getRetryAfterMs() ?? 3000; const waitMS = err.getRetryAfterMs() ?? 3000;
actionInterval = waitMS + 500; actionInterval = waitMS * 1.5;
await sleepForMs(waitMS); await sleepForMs(waitMS);
retryCount += 1; retryCount += 1;
await performAction(dataItem); await performAction(dataItem, index);
} }
}; };
@@ -333,7 +333,7 @@ export const rateLimitedActions = async <T, R = void>(
const dataItem = data[i]; const dataItem = data[i];
retryCount = 0; retryCount = 0;
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await performAction(dataItem); await performAction(dataItem, i);
if (actionInterval > 0) { if (actionInterval > 0) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await sleepForMs(actionInterval); await sleepForMs(actionInterval);

View File

@@ -1,6 +1,7 @@
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk'; import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
import { cryptoCallbacks } from './state/secretStorageKeys'; import { cryptoCallbacks } from './state/secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
type Session = { type Session = {
baseUrl: string; baseUrl: string;
@@ -46,6 +47,7 @@ export const startClient = async (mx: MatrixClient) => {
export const clearCacheAndReload = async (mx: MatrixClient) => { export const clearCacheAndReload = async (mx: MatrixClient) => {
mx.stopClient(); mx.stopClient();
clearNavToActivePathStore(mx.getSafeUserId());
await mx.store.deleteAllData(); await mx.store.deleteAllData();
window.location.reload(); window.location.reload();
}; };

View File

@@ -1,5 +1,5 @@
const cons = { const cons = {
version: '4.8.0', version: '4.8.1',
secretKey: { secretKey: {
ACCESS_TOKEN: 'cinny_access_token', ACCESS_TOKEN: 'cinny_access_token',
DEVICE_ID: 'cinny_device_id', DEVICE_ID: 'cinny_device_id',