mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-06 15:30:27 +03:00
URL navigation in interface and other improvements (#1633)
* load room on url change * add direct room list * render space room list * fix css syntax error * update scroll virtualizer * render subspaces room list * improve sidebar notification badge perf * add nav category components * add space recursive direct component * use nav category component in home, direct and space room list * add empty home and direct list layout * fix unread room menu ref * add more navigation items in room, direct and space tab * add more navigation * fix unread room menu to links * fix space lobby and search link * add explore navigation section * add notifications navigation menu * redirect to initial path after login * include unsupported room in rooms * move router hooks in hooks/router folder * add featured explore - WIP * load featured room with room summary * fix room card topic line clamp * add react query * load room summary using react query * add join button in room card * add content component * use content component in featured community content * fix content width * add responsive room card grid * fix async callback error status * add room card error button * fix client drawer shrink * add room topic viewer * open room card topic in viewer * fix room topic close btn * add get orphan parent util * add room card error dialog * add view featured room or space btn * refactor orphanParent to orphanParents * WIP - explore server * show space hint in room card * add room type filters * add per page item limit popout * reset scroll on public rooms load * refactor explore ui * refactor public rooms component * reset search on server change * fix typo * add empty featured section info * display user server on top * make server room card view btn clickable * add user server as default redirect for explore path * make home empty btn clickable * add thirdparty instance filter in server explore * remove since param on instance change * add server button in explore menu * rename notifications path to inbox * update react-virtual * Add notification messages inbox - WIP * add scroll top container component * add useInterval hook * add visibility change callback prop to scroll top container component * auto refresh notifications every 10 seconds * make message related component reusable * refactor matrix event renderer hoook * render notification message content * refactor matrix event renderer hook * update sequence card styles * move room navigate hook in global hooks * add open message button in notifications * add mark room as read button in notification group * show error in notification messages * add more featured spaces * render reply in notification messages * make notification message reply clickable * add outline prop for attachments * make old settings dialog viewable * add open featured communities as default config option * add invite count notification badge in sidebar and inbox menu * add element size observer hook * improve element size observer hook props * improve screen size hook * fix room avatar util function * allow Text props in Time component * fix dm room util function * add invitations * add no invites and notification cards * fix inbox tab unread badge visible without invite count * update folds and change inbox icon * memo search param construction * add message search in home * fix default message search order * fix display edited message new content * highlight search text in search messages * fix message search loading * disable log in production * add use space context * add useRoom context * fix space room list * fix inbox tab active state * add hook to get space child room recursive * add search for space * add virtual tile component * virtualize home and directs room list * update nav category component * use virtual tile component in more places * fix message highlight when click on reply twice * virtualize space room list * fix space room list lag issue * update folds * add room nav item component in space room list * use room nav item in home and direct room list * make space categories closable and save it in local storage * show unread room when category is collapsed * make home and direct room list category closable * rename room nav item show avatar prop * fix explore server category text alignment * rename closedRoomCategories to closedNavCategories * add nav category handler hook * save and restore last navigation path on space select * filter space rooms category by activity when it is closed * save and restore home and direct nav path state * save and restore inbox active path on open * save and restore explore tab active path * remove notification badge unread menu * add join room or space before navigate screen * move room component to features folder and add new room header * update folds * add room header menu * fix home room list activity sorting * do not hide selected room item on category closed in home and direct tab * replace old select room/tab call with navigate hook * improve state event hooks * show room card summary for joined rooms * prevent room from opening in wrong tab * only show message sender id on hover in modern layout * revert state event hooks changes * add key prop to room provider components * add welcome page * prevent excessive redirects * fix sidebar style with no spaces * move room settings in popup window * remove invite option from room settings * fix open room list search * add leave room prompt * standardize room and user avatar * fix avatar text size * add new reply layout * rename space hierarchy hook * add room topic hook * add room name hook * add room avatar hook and add direct room avatar util * space lobby - WIP * hide invalid space child event from space hierarchy in lobby * move lobby to features * fix element size observer hook width and height * add lobby header and hero section * add hierarchy room item error and loading state * add first and last child prop in sequence card * redirect to lobby from index path * memo and retry hierarchy room summary error * fix hierarchy room item styles * rename lobby hierarchy item card to room item card * show direct room avatar in space lobby * add hierarchy space item * add space item unknown room join button * fix space hierarchy hook refresh after new space join * change user avatar color and fallback render to user icon * change room avatar fallback to room icon * rename room/user avatar renderInitial prop to renderFallback * add room join and view button in space lobby * make power level api more reusable * fix space hierarchy not updating on child update * add menu to suggest or remove space children * show reply arrow in place of reply bend in message * fix typeerror in search because of wrong js-sdk t.ds * do not refetch hierarchy room summary on window focus * make room/user avatar un-draggable * change welcome page support button copy * drag-and-drop ordering of lobby spaces/rooms - WIP * add ASCIILexicalTable algorithms * fix wrong power level check in lobby items options * fix lobby can drop checks * fix join button error crash * fix reply spacing * fix m direct updated with other account data * add option to open room/space settings from lobby * add option in lobby to add new or existing room/spaces * fix room nav item selected styles * add space children reorder mechanism * fix space child reorder bug * fix hierarchy item sort function * Apply reorder of lobby into room list * add and improve space lobby menu items * add existing spaces menu in lobby * change restricted room allow params when dragging outside space * move featured servers config from homeserver list * removed unused features from space settings * add canonical alias as name fallback in lobby item * fix unreliable unread count update bug * fix after login redirect * fix room card topic hover style * Add dnd and folders in sidebar spaces * fix orphan space not visible in sidebar * fix sso login has mix of icon and button * fix space children not visible in home upon leaving space * recalculate notification on updating any space child * fix user color saturation/lightness * add user color to user avatar * add background colors to room avatar * show 2 length initial in sidebar space avatar * improve link color * add nav button component * open legacy create room and create direct * improve page route structure * handle hash router in path utils * mobile friendly router and navigation * make room header member drawer icon mobile friendly * setup index redirect for inbox and explore server route * add leave space prompt * improve member drawer filter menu * add space context menu * add context menu in home * add leave button in lobby items * render user tab avatar on sidebar * force overwrite netlify - test * netlify test * fix reset-password path without server redirected to login * add message link copy button in message menu * reset unread on sync prepared * fix stuck typing notifications * show typing indication in room nav item * refactor closedNavCategories atom to use userId in store key * refactor closedLobbyCategoriesAtom to include userId in store key * refactor navToActivePathAtom to use userId in storage key * remove unused file * refactor openedSidebarFolderAtom to include userId in storage key * add context menu for sidebar space tab * fix eslint not working * add option to pin/unpin child spaces * add context menu for directs tab * add context menu for direct and home tab * show lock icon for non-public space in header * increase matrix max listener count * wrap lobby add space room in callback hook
This commit is contained in:
parent
2b7d825694
commit
4c76a7fd18
290 changed files with 17447 additions and 3224 deletions
22
src/app/hooks/router/useDirectSelected.ts
Normal file
22
src/app/hooks/router/useDirectSelected.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { useMatch } from 'react-router-dom';
|
||||
import { getDirectCreatePath, getDirectPath } from '../../pages/pathUtils';
|
||||
|
||||
export const useDirectSelected = (): boolean => {
|
||||
const directMatch = useMatch({
|
||||
path: getDirectPath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!directMatch;
|
||||
};
|
||||
|
||||
export const useDirectCreateSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getDirectCreatePath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
28
src/app/hooks/router/useExploreSelected.ts
Normal file
28
src/app/hooks/router/useExploreSelected.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useMatch, useParams } from 'react-router-dom';
|
||||
import { getExploreFeaturedPath, getExplorePath } from '../../pages/pathUtils';
|
||||
|
||||
export const useExploreSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getExplorePath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
export const useExploreFeaturedSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getExploreFeaturedPath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
export const useExploreServer = (): string | undefined => {
|
||||
const { server } = useParams();
|
||||
|
||||
return server;
|
||||
};
|
||||
47
src/app/hooks/router/useHomeSelected.ts
Normal file
47
src/app/hooks/router/useHomeSelected.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useMatch } from 'react-router-dom';
|
||||
import {
|
||||
getHomeCreatePath,
|
||||
getHomeJoinPath,
|
||||
getHomePath,
|
||||
getHomeSearchPath,
|
||||
} from '../../pages/pathUtils';
|
||||
|
||||
export const useHomeSelected = (): boolean => {
|
||||
const homeMatch = useMatch({
|
||||
path: getHomePath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!homeMatch;
|
||||
};
|
||||
|
||||
export const useHomeCreateSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getHomeCreatePath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
export const useHomeJoinSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getHomeJoinPath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
export const useHomeSearchSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getHomeSearchPath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
36
src/app/hooks/router/useInbox.ts
Normal file
36
src/app/hooks/router/useInbox.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { useMatch } from 'react-router-dom';
|
||||
import {
|
||||
getInboxInvitesPath,
|
||||
getInboxNotificationsPath,
|
||||
getInboxPath,
|
||||
} from '../../pages/pathUtils';
|
||||
|
||||
export const useInboxSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getInboxPath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
export const useInboxNotificationsSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getInboxNotificationsPath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
export const useInboxInvitesSelected = (): boolean => {
|
||||
const match = useMatch({
|
||||
path: getInboxInvitesPath(),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
15
src/app/hooks/router/useSelectedRoom.ts
Normal file
15
src/app/hooks/router/useSelectedRoom.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { getCanonicalAliasRoomId, isRoomAlias } from '../../utils/matrix';
|
||||
import { useMatrixClient } from '../useMatrixClient';
|
||||
|
||||
export const useSelectedRoom = (): string | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const { roomIdOrAlias } = useParams();
|
||||
const roomId =
|
||||
roomIdOrAlias && isRoomAlias(roomIdOrAlias)
|
||||
? getCanonicalAliasRoomId(mx, roomIdOrAlias)
|
||||
: roomIdOrAlias;
|
||||
|
||||
return roomId;
|
||||
};
|
||||
37
src/app/hooks/router/useSelectedSpace.ts
Normal file
37
src/app/hooks/router/useSelectedSpace.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useMatch, useParams } from 'react-router-dom';
|
||||
import { getCanonicalAliasRoomId, isRoomAlias } from '../../utils/matrix';
|
||||
import { useMatrixClient } from '../useMatrixClient';
|
||||
import { getSpaceLobbyPath, getSpaceSearchPath } from '../../pages/pathUtils';
|
||||
|
||||
export const useSelectedSpace = (): string | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const { spaceIdOrAlias } = useParams();
|
||||
|
||||
const spaceId =
|
||||
spaceIdOrAlias && isRoomAlias(spaceIdOrAlias)
|
||||
? getCanonicalAliasRoomId(mx, spaceIdOrAlias)
|
||||
: spaceIdOrAlias;
|
||||
|
||||
return spaceId;
|
||||
};
|
||||
|
||||
export const useSpaceLobbySelected = (spaceIdOrAlias: string): boolean => {
|
||||
const match = useMatch({
|
||||
path: getSpaceLobbyPath(spaceIdOrAlias),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
export const useSpaceSearchSelected = (spaceIdOrAlias: string): boolean => {
|
||||
const match = useMatch({
|
||||
path: getSpaceSearchPath(spaceIdOrAlias),
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
14
src/app/hooks/useAccountDataCallback.ts
Normal file
14
src/app/hooks/useAccountDataCallback.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { ClientEvent, ClientEventHandlerMap, MatrixClient } from 'matrix-js-sdk';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useAccountDataCallback = (
|
||||
mx: MatrixClient,
|
||||
onAccountData: ClientEventHandlerMap[ClientEvent.AccountData]
|
||||
) => {
|
||||
useEffect(() => {
|
||||
mx.on(ClientEvent.AccountData, onAccountData);
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.AccountData, onAccountData);
|
||||
};
|
||||
}, [mx, onAccountData]);
|
||||
};
|
||||
|
|
@ -68,9 +68,11 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
|||
throw new Error('AsyncCallbackHook: Request replaced!');
|
||||
}
|
||||
if (alive()) {
|
||||
setState({
|
||||
status: AsyncStatus.Success,
|
||||
data,
|
||||
queueMicrotask(() => {
|
||||
setState({
|
||||
status: AsyncStatus.Success,
|
||||
data,
|
||||
});
|
||||
});
|
||||
}
|
||||
return data;
|
||||
|
|
@ -78,10 +80,13 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
|||
if (currentReqNumber !== reqNumberRef.current) {
|
||||
throw new Error('AsyncCallbackHook: Request replaced!');
|
||||
}
|
||||
|
||||
if (alive()) {
|
||||
setState({
|
||||
status: AsyncStatus.Error,
|
||||
error: e as TError,
|
||||
queueMicrotask(() => {
|
||||
setState({
|
||||
status: AsyncStatus.Error,
|
||||
error: e as TError,
|
||||
});
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
|
|
|
|||
12
src/app/hooks/useCapabilities.ts
Normal file
12
src/app/hooks/useCapabilities.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Capabilities } from 'matrix-js-sdk';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const CapabilitiesContext = createContext<Capabilities | null>(null);
|
||||
|
||||
export const CapabilitiesProvider = CapabilitiesContext.Provider;
|
||||
|
||||
export function useCapabilities(): Capabilities {
|
||||
const capabilities = useContext(CapabilitiesContext);
|
||||
if (!capabilities) throw new Error('Capabilities are not provided!');
|
||||
return capabilities;
|
||||
}
|
||||
27
src/app/hooks/useCategoryHandler.ts
Normal file
27
src/app/hooks/useCategoryHandler.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { MouseEventHandler } from 'react';
|
||||
|
||||
type CategoryAction =
|
||||
| {
|
||||
type: 'PUT';
|
||||
categoryId: string;
|
||||
}
|
||||
| {
|
||||
type: 'DELETE';
|
||||
categoryId: string;
|
||||
};
|
||||
export const useCategoryHandler = (
|
||||
setAtom: (action: CategoryAction) => void,
|
||||
closed: (categoryId: string) => boolean
|
||||
) => {
|
||||
const handleCategoryClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const categoryId = evt.currentTarget.getAttribute('data-category-id');
|
||||
if (!categoryId) return;
|
||||
if (closed(categoryId)) {
|
||||
setAtom({ type: 'DELETE', categoryId });
|
||||
return;
|
||||
}
|
||||
setAtom({ type: 'PUT', categoryId });
|
||||
};
|
||||
|
||||
return handleCategoryClick;
|
||||
};
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type HashRouterConfig = {
|
||||
enabled?: boolean;
|
||||
basename?: string;
|
||||
};
|
||||
|
||||
export type ClientConfig = {
|
||||
defaultHomeserver?: number;
|
||||
homeserverList?: string[];
|
||||
allowCustomHomeservers?: boolean;
|
||||
|
||||
hashRouter?: {
|
||||
enabled?: boolean;
|
||||
basename?: string;
|
||||
featuredCommunities?: {
|
||||
openAsDefault?: boolean;
|
||||
spaces?: string[];
|
||||
rooms?: string[];
|
||||
servers?: string[];
|
||||
};
|
||||
|
||||
hashRouter?: HashRouterConfig;
|
||||
};
|
||||
|
||||
const ClientConfigContext = createContext<ClientConfig | null>(null);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { MatrixClient, Room } from 'matrix-js-sdk';
|
||||
import { useMemo } from 'react';
|
||||
import { hasDMWith, isRoomAlias, isRoomId, isUserId } from '../utils/matrix';
|
||||
import { selectRoom } from '../../client/action/navigation';
|
||||
import { getDMRoomFor, isRoomAlias, isRoomId, isUserId } from '../utils/matrix';
|
||||
import { hasDevices } from '../../util/matrixUtil';
|
||||
import * as roomActions from '../../client/action/room';
|
||||
import { useRoomNavigate } from './useRoomNavigate';
|
||||
|
||||
export const SHRUG = '¯\\_(ツ)_/¯';
|
||||
|
||||
|
|
@ -59,6 +59,8 @@ export type CommandContent = {
|
|||
export type CommandRecord = Record<Command, CommandContent>;
|
||||
|
||||
export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
||||
const commands: CommandRecord = useMemo(
|
||||
() => ({
|
||||
[Command.Me]: {
|
||||
|
|
@ -84,16 +86,16 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
const userIds = rawIds.filter((id) => isUserId(id) && id !== mx.getUserId());
|
||||
if (userIds.length === 0) return;
|
||||
if (userIds.length === 1) {
|
||||
const dmRoomId = hasDMWith(mx, userIds[0]);
|
||||
const dmRoomId = getDMRoomFor(mx, userIds[0])?.roomId;
|
||||
if (dmRoomId) {
|
||||
selectRoom(dmRoomId);
|
||||
navigateRoom(dmRoomId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const devices = await Promise.all(userIds.map(hasDevices));
|
||||
const isEncrypt = devices.every((hasDevice) => hasDevice);
|
||||
const result = await roomActions.createDM(userIds, isEncrypt);
|
||||
selectRoom(result.room_id);
|
||||
navigateRoom(result.room_id);
|
||||
},
|
||||
},
|
||||
[Command.Join]: {
|
||||
|
|
@ -212,7 +214,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
|||
},
|
||||
},
|
||||
}),
|
||||
[mx, room]
|
||||
[mx, room, navigateRoom]
|
||||
);
|
||||
|
||||
return commands;
|
||||
|
|
|
|||
23
src/app/hooks/useElementSizeObserver.ts
Normal file
23
src/app/hooks/useElementSizeObserver.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { useCallback } from 'react';
|
||||
import { getResizeObserverEntry, useResizeObserver } from './useResizeObserver';
|
||||
|
||||
export const useElementSizeObserver = <T extends Element>(
|
||||
element: () => T | null,
|
||||
onResize: (width: number, height: number, element: T) => void
|
||||
) => {
|
||||
useResizeObserver(
|
||||
useCallback(
|
||||
(entries) => {
|
||||
const target = element();
|
||||
if (!target) return;
|
||||
const targetEntry = getResizeObserverEntry(target, entries);
|
||||
if (targetEntry) {
|
||||
const { clientWidth, clientHeight } = targetEntry.target;
|
||||
onResize(clientWidth, clientHeight, target);
|
||||
}
|
||||
},
|
||||
[element, onResize]
|
||||
),
|
||||
element
|
||||
);
|
||||
};
|
||||
24
src/app/hooks/useInterval.ts
Normal file
24
src/app/hooks/useInterval.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
export type IntervalCallback = () => void;
|
||||
|
||||
/**
|
||||
* @param callback interval callback.
|
||||
* @param ms interval time in milliseconds. negative value will stop the interval.
|
||||
* @returns interval id or undefined if not running.
|
||||
*/
|
||||
export const useInterval = (callback: IntervalCallback, ms: number): number | undefined => {
|
||||
const id = useMemo(() => {
|
||||
if (ms < 0) return undefined;
|
||||
return window.setInterval(callback, ms);
|
||||
}, [callback, ms]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
window.clearInterval(id);
|
||||
},
|
||||
[id]
|
||||
);
|
||||
|
||||
return id;
|
||||
};
|
||||
19
src/app/hooks/useJoinedRoomId.ts
Normal file
19
src/app/hooks/useJoinedRoomId.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
import { getCanonicalAliasRoomId, isRoomAlias } from '../utils/matrix';
|
||||
|
||||
export const useJoinedRoomId = (allRooms: string[], roomIdOrAlias: string): string | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const joinedRoomId = useMemo(() => {
|
||||
const roomId = isRoomAlias(roomIdOrAlias)
|
||||
? getCanonicalAliasRoomId(mx, roomIdOrAlias)
|
||||
: roomIdOrAlias;
|
||||
|
||||
if (roomId && allRooms.includes(roomId)) return roomId;
|
||||
return undefined;
|
||||
}, [mx, allRooms, roomIdOrAlias]);
|
||||
|
||||
return joinedRoomId;
|
||||
};
|
||||
44
src/app/hooks/useLocalRoomSummary.ts
Normal file
44
src/app/hooks/useLocalRoomSummary.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { GuestAccess, HistoryVisibility, JoinRule, Room } from 'matrix-js-sdk';
|
||||
import { getStateEvent } from '../utils/room';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
|
||||
export type LocalRoomSummary = {
|
||||
roomId: string;
|
||||
name: string;
|
||||
topic?: string;
|
||||
avatarUrl?: string;
|
||||
canonicalAlias?: string;
|
||||
worldReadable?: boolean;
|
||||
guestCanJoin?: boolean;
|
||||
memberCount?: number;
|
||||
roomType?: string;
|
||||
joinRule?: JoinRule;
|
||||
};
|
||||
export const useLocalRoomSummary = (room: Room): LocalRoomSummary => {
|
||||
const topicEvent = getStateEvent(room, StateEvent.RoomTopic);
|
||||
const topicContent = topicEvent?.getContent();
|
||||
const topic =
|
||||
topicContent && typeof topicContent.topic === 'string' ? topicContent.topic : undefined;
|
||||
|
||||
const historyEvent = getStateEvent(room, StateEvent.RoomHistoryVisibility);
|
||||
const historyContent = historyEvent?.getContent();
|
||||
const worldReadable =
|
||||
historyContent && typeof historyContent.history_visibility === 'string'
|
||||
? historyContent.history_visibility === HistoryVisibility.WorldReadable
|
||||
: undefined;
|
||||
|
||||
const guestCanJoin = room.getGuestAccess() === GuestAccess.CanJoin;
|
||||
|
||||
return {
|
||||
roomId: room.roomId,
|
||||
name: room.name,
|
||||
topic,
|
||||
avatarUrl: room.getMxcAvatarUrl() ?? undefined,
|
||||
canonicalAlias: room.getCanonicalAlias() ?? undefined,
|
||||
worldReadable,
|
||||
guestCanJoin,
|
||||
memberCount: room.getJoinedMemberCount(),
|
||||
roomType: room.getType(),
|
||||
joinRule: room.getJoinRule(),
|
||||
};
|
||||
};
|
||||
|
|
@ -1,80 +1,31 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { MatrixEvent } from 'matrix-js-sdk';
|
||||
import { MessageEvent, StateEvent } from '../../types/matrix/room';
|
||||
|
||||
export type EventRenderer<T extends unknown[]> = (
|
||||
eventId: string,
|
||||
mEvent: MatrixEvent,
|
||||
...args: T
|
||||
) => ReactNode;
|
||||
export type EventRenderer<T extends unknown[]> = (...args: T) => ReactNode;
|
||||
|
||||
export type EventRendererOpts<T extends unknown[]> = {
|
||||
renderRoomMessage?: EventRenderer<T>;
|
||||
renderRoomEncrypted?: EventRenderer<T>;
|
||||
renderSticker?: EventRenderer<T>;
|
||||
renderRoomMember?: EventRenderer<T>;
|
||||
renderRoomName?: EventRenderer<T>;
|
||||
renderRoomTopic?: EventRenderer<T>;
|
||||
renderRoomAvatar?: EventRenderer<T>;
|
||||
renderStateEvent?: EventRenderer<T>;
|
||||
renderEvent?: EventRenderer<T>;
|
||||
};
|
||||
export type EventRendererOpts<T extends unknown[]> = Record<string, EventRenderer<T>>;
|
||||
|
||||
export type RenderMatrixEvent<T extends unknown[]> = (
|
||||
eventId: string,
|
||||
mEvent: MatrixEvent,
|
||||
eventType: string,
|
||||
isStateEvent: boolean,
|
||||
...args: T
|
||||
) => ReactNode;
|
||||
|
||||
export const useMatrixEventRenderer =
|
||||
<T extends unknown[]>({
|
||||
renderRoomMessage,
|
||||
renderRoomEncrypted,
|
||||
renderSticker,
|
||||
renderRoomMember,
|
||||
renderRoomName,
|
||||
renderRoomTopic,
|
||||
renderRoomAvatar,
|
||||
renderStateEvent,
|
||||
renderEvent,
|
||||
}: EventRendererOpts<T>): RenderMatrixEvent<T> =>
|
||||
(eventId, mEvent, ...args) => {
|
||||
const eventType = mEvent.getWireType();
|
||||
<T extends unknown[]>(
|
||||
typeToRenderer: EventRendererOpts<T>,
|
||||
renderStateEvent?: EventRenderer<T>,
|
||||
renderEvent?: EventRenderer<T>
|
||||
): RenderMatrixEvent<T> =>
|
||||
(eventType, isStateEvent, ...args) => {
|
||||
const renderer = typeToRenderer[eventType];
|
||||
if (typeToRenderer[eventType]) return renderer(...args);
|
||||
|
||||
if (eventType === MessageEvent.RoomMessage && renderRoomMessage) {
|
||||
return renderRoomMessage(eventId, mEvent, ...args);
|
||||
if (isStateEvent && renderStateEvent) {
|
||||
return renderStateEvent(...args);
|
||||
}
|
||||
|
||||
if (eventType === MessageEvent.RoomMessageEncrypted && renderRoomEncrypted) {
|
||||
return renderRoomEncrypted(eventId, mEvent, ...args);
|
||||
}
|
||||
|
||||
if (eventType === MessageEvent.Sticker && renderSticker) {
|
||||
return renderSticker(eventId, mEvent, ...args);
|
||||
}
|
||||
|
||||
if (eventType === StateEvent.RoomMember && renderRoomMember) {
|
||||
return renderRoomMember(eventId, mEvent, ...args);
|
||||
}
|
||||
|
||||
if (eventType === StateEvent.RoomName && renderRoomName) {
|
||||
return renderRoomName(eventId, mEvent, ...args);
|
||||
}
|
||||
|
||||
if (eventType === StateEvent.RoomTopic && renderRoomTopic) {
|
||||
return renderRoomTopic(eventId, mEvent, ...args);
|
||||
}
|
||||
|
||||
if (eventType === StateEvent.RoomAvatar && renderRoomAvatar) {
|
||||
return renderRoomAvatar(eventId, mEvent, ...args);
|
||||
}
|
||||
|
||||
if (typeof mEvent.getStateKey() === 'string' && renderStateEvent) {
|
||||
return renderStateEvent(eventId, mEvent, ...args);
|
||||
}
|
||||
|
||||
if (typeof mEvent.getStateKey() !== 'string' && renderEvent) {
|
||||
return renderEvent(eventId, mEvent, ...args);
|
||||
if (!isStateEvent && renderEvent) {
|
||||
return renderEvent(...args);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
16
src/app/hooks/useMediaConfig.ts
Normal file
16
src/app/hooks/useMediaConfig.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface MediaConfig {
|
||||
[key: string]: unknown;
|
||||
'm.upload.size'?: number;
|
||||
}
|
||||
|
||||
const MediaConfigContext = createContext<MediaConfig | null>(null);
|
||||
|
||||
export const MediaConfigProvider = MediaConfigContext.Provider;
|
||||
|
||||
export function useMediaConfig(): MediaConfig {
|
||||
const mediaConfig = useContext(MediaConfigContext);
|
||||
if (!mediaConfig) throw new Error('Media configs are not provided!');
|
||||
return mediaConfig;
|
||||
}
|
||||
18
src/app/hooks/useNavToActivePathMapper.ts
Normal file
18
src/app/hooks/useNavToActivePathMapper.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useNavToActivePathAtom } from '../state/hooks/navToActivePath';
|
||||
|
||||
export const useNavToActivePathMapper = (navId: string) => {
|
||||
const location = useLocation();
|
||||
const setNavToActivePath = useSetAtom(useNavToActivePathAtom());
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname, search, hash } = location;
|
||||
setNavToActivePath({
|
||||
type: 'PUT',
|
||||
navId,
|
||||
path: { pathname, search, hash },
|
||||
});
|
||||
}, [location, setNavToActivePath, navId]);
|
||||
};
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
import { Room } from 'matrix-js-sdk';
|
||||
import { createContext, useCallback, useContext } from 'react';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
import { useStateEvent } from './useStateEvent';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { useForceUpdate } from './useForceUpdate';
|
||||
import { useStateEventCallback } from './useStateEventCallback';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { getStateEvent } from '../utils/room';
|
||||
|
||||
export type PowerLevelActions = 'invite' | 'redact' | 'kick' | 'ban' | 'historical';
|
||||
|
||||
|
|
@ -16,7 +20,7 @@ enum DefaultPowerLevels {
|
|||
historical = 0,
|
||||
}
|
||||
|
||||
interface IPowerLevels {
|
||||
export interface IPowerLevels {
|
||||
users_default?: number;
|
||||
state_default?: number;
|
||||
events_default?: number;
|
||||
|
|
@ -31,9 +35,75 @@ interface IPowerLevels {
|
|||
notifications?: Record<string, number>;
|
||||
}
|
||||
|
||||
export type GetPowerLevel = (userId: string) => number;
|
||||
export type CanSend = (eventType: string | undefined, powerLevel: number) => boolean;
|
||||
export type CanDoAction = (action: PowerLevelActions, powerLevel: number) => boolean;
|
||||
export function usePowerLevels(room: Room): IPowerLevels {
|
||||
const powerLevelsEvent = useStateEvent(room, StateEvent.RoomPowerLevels);
|
||||
const powerLevels: IPowerLevels =
|
||||
powerLevelsEvent?.getContent<IPowerLevels>() ?? DefaultPowerLevels;
|
||||
|
||||
return powerLevels;
|
||||
}
|
||||
|
||||
export const PowerLevelsContext = createContext<IPowerLevels | null>(null);
|
||||
|
||||
export const PowerLevelsContextProvider = PowerLevelsContext.Provider;
|
||||
|
||||
export const usePowerLevelsContext = (): IPowerLevels => {
|
||||
const pl = useContext(PowerLevelsContext);
|
||||
if (!pl) throw new Error('PowerLevelContext is not initialized!');
|
||||
return pl;
|
||||
};
|
||||
|
||||
export const useRoomsPowerLevels = (rooms: Room[]): Map<string, IPowerLevels> => {
|
||||
const mx = useMatrixClient();
|
||||
const [updateCount, forceUpdate] = useForceUpdate();
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(event) => {
|
||||
const roomId = event.getRoomId();
|
||||
if (
|
||||
roomId &&
|
||||
event.getType() === StateEvent.RoomPowerLevels &&
|
||||
event.getStateKey() === '' &&
|
||||
rooms.find((r) => r.roomId === roomId)
|
||||
) {
|
||||
forceUpdate();
|
||||
}
|
||||
},
|
||||
[rooms, forceUpdate]
|
||||
)
|
||||
);
|
||||
|
||||
const roomToPowerLevels = useMemo(
|
||||
() => {
|
||||
const rToPl = new Map<string, IPowerLevels>();
|
||||
|
||||
rooms.forEach((room) => {
|
||||
const pl = getStateEvent(room, StateEvent.RoomPowerLevels, '')?.getContent<IPowerLevels>();
|
||||
if (pl) rToPl.set(room.roomId, pl);
|
||||
});
|
||||
|
||||
return rToPl;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[rooms, updateCount]
|
||||
);
|
||||
|
||||
return roomToPowerLevels;
|
||||
};
|
||||
|
||||
export type GetPowerLevel = (powerLevels: IPowerLevels, userId: string | undefined) => number;
|
||||
export type CanSend = (
|
||||
powerLevels: IPowerLevels,
|
||||
eventType: string | undefined,
|
||||
powerLevel: number
|
||||
) => boolean;
|
||||
export type CanDoAction = (
|
||||
powerLevels: IPowerLevels,
|
||||
action: PowerLevelActions,
|
||||
powerLevel: number
|
||||
) => boolean;
|
||||
|
||||
export type PowerLevelsAPI = {
|
||||
getPowerLevel: GetPowerLevel;
|
||||
|
|
@ -42,51 +112,58 @@ export type PowerLevelsAPI = {
|
|||
canDoAction: CanDoAction;
|
||||
};
|
||||
|
||||
export function usePowerLevels(room: Room): PowerLevelsAPI {
|
||||
const powerLevelsEvent = useStateEvent(room, StateEvent.RoomPowerLevels);
|
||||
const powerLevels: IPowerLevels = powerLevelsEvent?.getContent() ?? DefaultPowerLevels;
|
||||
export const powerLevelAPI: PowerLevelsAPI = {
|
||||
getPowerLevel: (powerLevels, userId) => {
|
||||
const { users_default: usersDefault, users } = powerLevels;
|
||||
if (userId && users && typeof users[userId] === 'number') {
|
||||
return users[userId];
|
||||
}
|
||||
return usersDefault ?? DefaultPowerLevels.usersDefault;
|
||||
},
|
||||
canSendEvent: (powerLevels, eventType, powerLevel) => {
|
||||
const { events, events_default: eventsDefault } = powerLevels;
|
||||
if (events && eventType && typeof events[eventType] === 'number') {
|
||||
return powerLevel >= events[eventType];
|
||||
}
|
||||
return powerLevel >= (eventsDefault ?? DefaultPowerLevels.eventsDefault);
|
||||
},
|
||||
canSendStateEvent: (powerLevels, eventType, powerLevel) => {
|
||||
const { events, state_default: stateDefault } = powerLevels;
|
||||
if (events && eventType && typeof events[eventType] === 'number') {
|
||||
return powerLevel >= events[eventType];
|
||||
}
|
||||
return powerLevel >= (stateDefault ?? DefaultPowerLevels.stateDefault);
|
||||
},
|
||||
canDoAction: (powerLevels, action, powerLevel) => {
|
||||
const requiredPL = powerLevels[action];
|
||||
if (typeof requiredPL === 'number') {
|
||||
return powerLevel >= requiredPL;
|
||||
}
|
||||
return powerLevel >= DefaultPowerLevels[action];
|
||||
},
|
||||
};
|
||||
|
||||
const getPowerLevel: GetPowerLevel = useCallback(
|
||||
(userId) => {
|
||||
const { users_default: usersDefault, users } = powerLevels;
|
||||
if (users && typeof users[userId] === 'number') {
|
||||
return users[userId];
|
||||
}
|
||||
return usersDefault ?? DefaultPowerLevels.usersDefault;
|
||||
},
|
||||
export const usePowerLevelsAPI = (powerLevels: IPowerLevels) => {
|
||||
const getPowerLevel = useCallback(
|
||||
(userId: string | undefined) => powerLevelAPI.getPowerLevel(powerLevels, userId),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
const canSendEvent: CanSend = useCallback(
|
||||
(eventType, powerLevel) => {
|
||||
const { events, events_default: eventsDefault } = powerLevels;
|
||||
if (events && eventType && typeof events[eventType] === 'number') {
|
||||
return powerLevel >= events[eventType];
|
||||
}
|
||||
return powerLevel >= (eventsDefault ?? DefaultPowerLevels.eventsDefault);
|
||||
},
|
||||
const canSendEvent = useCallback(
|
||||
(eventType: string | undefined, powerLevel: number) =>
|
||||
powerLevelAPI.canSendEvent(powerLevels, eventType, powerLevel),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
const canSendStateEvent: CanSend = useCallback(
|
||||
(eventType, powerLevel) => {
|
||||
const { events, state_default: stateDefault } = powerLevels;
|
||||
if (events && eventType && typeof events[eventType] === 'number') {
|
||||
return powerLevel >= events[eventType];
|
||||
}
|
||||
return powerLevel >= (stateDefault ?? DefaultPowerLevels.stateDefault);
|
||||
},
|
||||
const canSendStateEvent = useCallback(
|
||||
(eventType: string | undefined, powerLevel: number) =>
|
||||
powerLevelAPI.canSendStateEvent(powerLevels, eventType, powerLevel),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
const canDoAction: CanDoAction = useCallback(
|
||||
(action, powerLevel) => {
|
||||
const requiredPL = powerLevels[action];
|
||||
if (typeof requiredPL === 'number') {
|
||||
return powerLevel >= requiredPL;
|
||||
}
|
||||
return powerLevel >= DefaultPowerLevels[action];
|
||||
},
|
||||
const canDoAction = useCallback(
|
||||
(action: PowerLevelActions, powerLevel: number) =>
|
||||
powerLevelAPI.canDoAction(powerLevels, action, powerLevel),
|
||||
[powerLevels]
|
||||
);
|
||||
|
||||
|
|
@ -96,14 +173,4 @@ export function usePowerLevels(room: Room): PowerLevelsAPI {
|
|||
canSendStateEvent,
|
||||
canDoAction,
|
||||
};
|
||||
}
|
||||
|
||||
export const PowerLevelsContext = createContext<PowerLevelsAPI | null>(null);
|
||||
|
||||
export const PowerLevelsContextProvider = PowerLevelsContext.Provider;
|
||||
|
||||
export const usePowerLevelsAPI = (): PowerLevelsAPI => {
|
||||
const api = useContext(PowerLevelsContext);
|
||||
if (!api) throw new Error('PowerLevelContext is not initialized!');
|
||||
return api;
|
||||
};
|
||||
|
|
|
|||
12
src/app/hooks/useRoom.ts
Normal file
12
src/app/hooks/useRoom.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Room } from 'matrix-js-sdk';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const RoomContext = createContext<Room | null>(null);
|
||||
|
||||
export const RoomProvider = RoomContext.Provider;
|
||||
|
||||
export function useRoom(): Room {
|
||||
const room = useContext(RoomContext);
|
||||
if (!room) throw new Error('Room not provided!');
|
||||
return room;
|
||||
}
|
||||
41
src/app/hooks/useRoomMeta.ts
Normal file
41
src/app/hooks/useRoomMeta.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Room, RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { useStateEvent } from './useStateEvent';
|
||||
|
||||
export const useRoomAvatar = (room: Room, dm?: boolean): string | undefined => {
|
||||
const avatarEvent = useStateEvent(room, StateEvent.RoomAvatar);
|
||||
|
||||
if (dm) {
|
||||
return room.getAvatarFallbackMember()?.getMxcAvatarUrl();
|
||||
}
|
||||
const content = avatarEvent?.getContent();
|
||||
const avatarMxc = content && typeof content.url === 'string' ? content.url : undefined;
|
||||
|
||||
return avatarMxc;
|
||||
};
|
||||
|
||||
export const useRoomName = (room: Room): string => {
|
||||
const [name, setName] = useState(room.name);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRoomNameChange: RoomEventHandlerMap[RoomEvent.Name] = () => {
|
||||
setName(room.name);
|
||||
};
|
||||
room.on(RoomEvent.Name, handleRoomNameChange);
|
||||
return () => {
|
||||
room.removeListener(RoomEvent.Name, handleRoomNameChange);
|
||||
};
|
||||
}, [room]);
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const useRoomTopic = (room: Room): string | undefined => {
|
||||
const topicEvent = useStateEvent(room, StateEvent.RoomTopic);
|
||||
|
||||
const content = topicEvent?.getContent();
|
||||
const topic = content && typeof content.topic === 'string' ? content.topic : undefined;
|
||||
|
||||
return topic;
|
||||
};
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { MatrixEvent, MsgType } from 'matrix-js-sdk';
|
||||
|
||||
export type MsgContentRenderer<T extends unknown[]> = (
|
||||
eventId: string,
|
||||
mEvent: MatrixEvent,
|
||||
...args: T
|
||||
) => ReactNode;
|
||||
|
||||
export type RoomMsgContentRendererOpts<T extends unknown[]> = {
|
||||
renderText?: MsgContentRenderer<T>;
|
||||
renderEmote?: MsgContentRenderer<T>;
|
||||
renderNotice?: MsgContentRenderer<T>;
|
||||
renderImage?: MsgContentRenderer<T>;
|
||||
renderVideo?: MsgContentRenderer<T>;
|
||||
renderAudio?: MsgContentRenderer<T>;
|
||||
renderFile?: MsgContentRenderer<T>;
|
||||
renderLocation?: MsgContentRenderer<T>;
|
||||
renderBadEncrypted?: MsgContentRenderer<T>;
|
||||
renderUnsupported?: MsgContentRenderer<T>;
|
||||
renderBrokenFallback?: MsgContentRenderer<T>;
|
||||
};
|
||||
|
||||
export type RenderRoomMsgContent<T extends unknown[]> = (
|
||||
eventId: string,
|
||||
mEvent: MatrixEvent,
|
||||
...args: T
|
||||
) => ReactNode;
|
||||
|
||||
export const useRoomMsgContentRenderer =
|
||||
<T extends unknown[]>({
|
||||
renderText,
|
||||
renderEmote,
|
||||
renderNotice,
|
||||
renderImage,
|
||||
renderVideo,
|
||||
renderAudio,
|
||||
renderFile,
|
||||
renderLocation,
|
||||
renderBadEncrypted,
|
||||
renderUnsupported,
|
||||
renderBrokenFallback,
|
||||
}: RoomMsgContentRendererOpts<T>): RenderRoomMsgContent<T> =>
|
||||
(eventId, mEvent, ...args) => {
|
||||
const msgType = mEvent.getContent().msgtype;
|
||||
|
||||
let node: ReactNode = null;
|
||||
|
||||
if (msgType === MsgType.Text && renderText) node = renderText(eventId, mEvent, ...args);
|
||||
else if (msgType === MsgType.Emote && renderEmote) node = renderEmote(eventId, mEvent, ...args);
|
||||
else if (msgType === MsgType.Notice && renderNotice)
|
||||
node = renderNotice(eventId, mEvent, ...args);
|
||||
else if (msgType === MsgType.Image && renderImage) node = renderImage(eventId, mEvent, ...args);
|
||||
else if (msgType === MsgType.Video && renderVideo) node = renderVideo(eventId, mEvent, ...args);
|
||||
else if (msgType === MsgType.Audio && renderAudio) node = renderAudio(eventId, mEvent, ...args);
|
||||
else if (msgType === MsgType.File && renderFile) node = renderFile(eventId, mEvent, ...args);
|
||||
else if (msgType === MsgType.Location && renderLocation)
|
||||
node = renderLocation(eventId, mEvent, ...args);
|
||||
else if (msgType === 'm.bad.encrypted' && renderBadEncrypted)
|
||||
node = renderBadEncrypted(eventId, mEvent, ...args);
|
||||
else if (renderUnsupported) {
|
||||
node = renderUnsupported(eventId, mEvent, ...args);
|
||||
}
|
||||
|
||||
if (!node && renderBrokenFallback) node = renderBrokenFallback(eventId, mEvent, ...args);
|
||||
|
||||
return node;
|
||||
};
|
||||
55
src/app/hooks/useRoomNavigate.ts
Normal file
55
src/app/hooks/useRoomNavigate.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { getCanonicalAliasOrRoomId } from '../utils/matrix';
|
||||
import {
|
||||
getDirectRoomPath,
|
||||
getHomeRoomPath,
|
||||
getSpacePath,
|
||||
getSpaceRoomPath,
|
||||
} from '../pages/pathUtils';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { getOrphanParents } from '../utils/room';
|
||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||
import { mDirectAtom } from '../state/mDirectList';
|
||||
|
||||
export const useRoomNavigate = () => {
|
||||
const navigate = useNavigate();
|
||||
const mx = useMatrixClient();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
const navigateSpace = useCallback(
|
||||
(roomId: string) => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
||||
navigate(getSpacePath(roomIdOrAlias));
|
||||
},
|
||||
[mx, navigate]
|
||||
);
|
||||
|
||||
const navigateRoom = useCallback(
|
||||
(roomId: string, eventId?: string) => {
|
||||
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, roomId);
|
||||
|
||||
const orphanParents = getOrphanParents(roomToParents, roomId);
|
||||
if (orphanParents.length > 0) {
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, orphanParents[0]);
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, roomIdOrAlias, eventId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDirects.has(roomId)) {
|
||||
navigate(getDirectRoomPath(roomIdOrAlias, eventId));
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(getHomeRoomPath(roomIdOrAlias, eventId));
|
||||
},
|
||||
[mx, navigate, roomToParents, mDirects]
|
||||
);
|
||||
|
||||
return {
|
||||
navigateSpace,
|
||||
navigateRoom,
|
||||
};
|
||||
};
|
||||
10
src/app/hooks/useRoomTypingMembers.ts
Normal file
10
src/app/hooks/useRoomTypingMembers.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { useAtomValue } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
import { roomIdToTypingMembersAtom, selectRoomTypingMembersAtom } from '../state/typingMembers';
|
||||
|
||||
export const useRoomTypingMember = (roomId: string) => {
|
||||
const typing = useAtomValue(
|
||||
useMemo(() => selectRoomTypingMembersAtom(roomId, roomIdToTypingMembersAtom), [roomId])
|
||||
);
|
||||
return typing;
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import { getResizeObserverEntry, useResizeObserver } from './useResizeObserver';
|
||||
import { createContext, useCallback, useContext, useState } from 'react';
|
||||
import { useElementSizeObserver } from './useElementSizeObserver';
|
||||
|
||||
export const TABLET_BREAKPOINT = 1124;
|
||||
export const MOBILE_BREAKPOINT = 750;
|
||||
|
|
@ -16,21 +16,24 @@ export const getScreenSize = (width: number): ScreenSize => {
|
|||
return ScreenSize.Mobile;
|
||||
};
|
||||
|
||||
export const useScreenSize = (): [ScreenSize, number] => {
|
||||
const [size, setSize] = useState<[ScreenSize, number]>([
|
||||
getScreenSize(document.body.clientWidth),
|
||||
document.body.clientWidth,
|
||||
]);
|
||||
useResizeObserver(
|
||||
useCallback((entries) => {
|
||||
const bodyEntry = getResizeObserverEntry(document.body, entries);
|
||||
if (bodyEntry) {
|
||||
const bWidth = bodyEntry.contentRect.width;
|
||||
setSize([getScreenSize(bWidth), bWidth]);
|
||||
}
|
||||
}, []),
|
||||
document.body
|
||||
export const useScreenSize = (): ScreenSize => {
|
||||
const [size, setSize] = useState<ScreenSize>(getScreenSize(document.body.clientWidth));
|
||||
|
||||
useElementSizeObserver(
|
||||
useCallback(() => document.body, []),
|
||||
useCallback((width) => setSize(getScreenSize(width)), [])
|
||||
);
|
||||
|
||||
return size;
|
||||
};
|
||||
|
||||
const ScreenSizeContext = createContext<ScreenSize | null>(null);
|
||||
export const ScreenSizeProvider = ScreenSizeContext.Provider;
|
||||
|
||||
export const useScreenSizeContext = (): ScreenSize => {
|
||||
const screenSize = useContext(ScreenSizeContext);
|
||||
if (screenSize === null) {
|
||||
throw new Error('Screen size not provided!');
|
||||
}
|
||||
return screenSize;
|
||||
};
|
||||
|
|
|
|||
138
src/app/hooks/useSidebarItems.ts
Normal file
138
src/app/hooks/useSidebarItems.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { getAccountData, isSpace } from '../utils/room';
|
||||
import { Membership } from '../../types/matrix/room';
|
||||
import { useAccountDataCallback } from './useAccountDataCallback';
|
||||
|
||||
export type ISidebarFolder = {
|
||||
name?: string;
|
||||
id: string;
|
||||
content: string[];
|
||||
};
|
||||
export type TSidebarItem = string | ISidebarFolder;
|
||||
export type SidebarItems = Array<TSidebarItem>;
|
||||
|
||||
export type InCinnySpacesContent = {
|
||||
shortcut?: string[];
|
||||
sidebar?: SidebarItems;
|
||||
};
|
||||
|
||||
export const parseSidebar = (
|
||||
mx: MatrixClient,
|
||||
orphanSpaces: string[],
|
||||
content?: InCinnySpacesContent
|
||||
) => {
|
||||
const sidebar = content?.sidebar ?? content?.shortcut ?? [];
|
||||
const orphans = new Set(orphanSpaces);
|
||||
|
||||
const items: SidebarItems = [];
|
||||
|
||||
const safeToAdd = (spaceId: string): boolean => {
|
||||
if (typeof spaceId !== 'string') return false;
|
||||
const space = mx.getRoom(spaceId);
|
||||
if (space?.getMyMembership() !== Membership.Join) return false;
|
||||
return isSpace(space);
|
||||
};
|
||||
|
||||
sidebar.forEach((item) => {
|
||||
if (typeof item === 'string') {
|
||||
if (safeToAdd(item) && !items.includes(item)) {
|
||||
orphans.delete(item);
|
||||
items.push(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
typeof item === 'object' &&
|
||||
typeof item.id === 'string' &&
|
||||
Array.isArray(item.content) &&
|
||||
!items.find((i) => (typeof i === 'string' ? false : i.id === item.id))
|
||||
) {
|
||||
const safeContent = item.content.filter(safeToAdd);
|
||||
safeContent.forEach((i) => orphans.delete(i));
|
||||
items.push({
|
||||
...item,
|
||||
content: Array.from(new Set(safeContent)),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
orphans.forEach((spaceId) => items.push(spaceId));
|
||||
return items;
|
||||
};
|
||||
|
||||
export const useSidebarItems = (
|
||||
orphanSpaces: string[]
|
||||
): [SidebarItems, Dispatch<SetStateAction<SidebarItems>>] => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [sidebarItems, setSidebarItems] = useState(() => {
|
||||
const inCinnySpacesContent = getAccountData(
|
||||
mx,
|
||||
AccountDataEvent.CinnySpaces
|
||||
)?.getContent<InCinnySpacesContent>();
|
||||
return parseSidebar(mx, orphanSpaces, inCinnySpacesContent);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const inCinnySpacesContent = getAccountData(
|
||||
mx,
|
||||
AccountDataEvent.CinnySpaces
|
||||
)?.getContent<InCinnySpacesContent>();
|
||||
setSidebarItems(parseSidebar(mx, orphanSpaces, inCinnySpacesContent));
|
||||
}, [mx, orphanSpaces]);
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (mEvent.getType() === AccountDataEvent.CinnySpaces) {
|
||||
const newContent = mEvent.getContent<InCinnySpacesContent>();
|
||||
setSidebarItems(parseSidebar(mx, orphanSpaces, newContent));
|
||||
}
|
||||
},
|
||||
[mx, orphanSpaces]
|
||||
)
|
||||
);
|
||||
|
||||
return [sidebarItems, setSidebarItems];
|
||||
};
|
||||
|
||||
export const sidebarItemWithout = (items: SidebarItems, roomId: string) => {
|
||||
const newItems: SidebarItems = items
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
if (item === roomId) return null;
|
||||
return item;
|
||||
}
|
||||
if (item.content.includes(roomId)) {
|
||||
const newContent = item.content.filter((id) => id !== roomId);
|
||||
if (newContent.length === 0) return null;
|
||||
return {
|
||||
...item,
|
||||
content: newContent,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item !== null) as SidebarItems;
|
||||
|
||||
return newItems;
|
||||
};
|
||||
|
||||
export const makeCinnySpacesContent = (
|
||||
mx: MatrixClient,
|
||||
items: SidebarItems
|
||||
): InCinnySpacesContent => {
|
||||
const currentInSpaces =
|
||||
getAccountData(mx, AccountDataEvent.CinnySpaces)?.getContent<InCinnySpacesContent>() ?? {};
|
||||
|
||||
const newSpacesContent: InCinnySpacesContent = {
|
||||
...currentInSpaces,
|
||||
sidebar: items,
|
||||
};
|
||||
|
||||
return newSpacesContent;
|
||||
};
|
||||
17
src/app/hooks/useSpace.ts
Normal file
17
src/app/hooks/useSpace.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Room } from 'matrix-js-sdk';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const SpaceContext = createContext<Room | null>(null);
|
||||
|
||||
export const SpaceProvider = SpaceContext.Provider;
|
||||
|
||||
export function useSpace(): Room {
|
||||
const space = useContext(SpaceContext);
|
||||
if (!space) throw new Error('Space not provided!');
|
||||
return space;
|
||||
}
|
||||
|
||||
export function useSpaceOptionally(): Room | null {
|
||||
const space = useContext(SpaceContext);
|
||||
return space;
|
||||
}
|
||||
253
src/app/hooks/useSpaceHierarchy.ts
Normal file
253
src/app/hooks/useSpaceHierarchy.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { roomToParentsAtom } from '../state/room/roomToParents';
|
||||
import { MSpaceChildContent, StateEvent } from '../../types/matrix/room';
|
||||
import { getAllParents, getStateEvents, isValidChild } from '../utils/room';
|
||||
import { isRoomId } from '../utils/matrix';
|
||||
import { SortFunc, byOrderKey, byTsOldToNew, factoryRoomIdByActivity } from '../utils/sort';
|
||||
import { useStateEventCallback } from './useStateEventCallback';
|
||||
|
||||
export type HierarchyItem =
|
||||
| {
|
||||
roomId: string;
|
||||
content: MSpaceChildContent;
|
||||
ts: number;
|
||||
space: true;
|
||||
parentId?: string;
|
||||
}
|
||||
| {
|
||||
roomId: string;
|
||||
content: MSpaceChildContent;
|
||||
ts: number;
|
||||
space?: false;
|
||||
parentId: string;
|
||||
};
|
||||
|
||||
type GetRoomCallback = (roomId: string) => Room | undefined;
|
||||
|
||||
const hierarchyItemTs: SortFunc<HierarchyItem> = (a, b) => byTsOldToNew(a.ts, b.ts);
|
||||
const hierarchyItemByOrder: SortFunc<HierarchyItem> = (a, b) =>
|
||||
byOrderKey(a.content.order, b.content.order);
|
||||
|
||||
const getHierarchySpaces = (
|
||||
rootSpaceId: string,
|
||||
getRoom: GetRoomCallback,
|
||||
spaceRooms: Set<string>
|
||||
): HierarchyItem[] => {
|
||||
const rootSpaceItem: HierarchyItem = {
|
||||
roomId: rootSpaceId,
|
||||
content: { via: [] },
|
||||
ts: 0,
|
||||
space: true,
|
||||
};
|
||||
let spaceItems: HierarchyItem[] = [];
|
||||
|
||||
const findAndCollectHierarchySpaces = (spaceItem: HierarchyItem) => {
|
||||
if (spaceItems.find((item) => item.roomId === spaceItem.roomId)) return;
|
||||
const space = getRoom(spaceItem.roomId);
|
||||
spaceItems.push(spaceItem);
|
||||
|
||||
if (!space) return;
|
||||
const childEvents = getStateEvents(space, StateEvent.SpaceChild);
|
||||
|
||||
childEvents.forEach((childEvent) => {
|
||||
if (!isValidChild(childEvent)) return;
|
||||
const childId = childEvent.getStateKey();
|
||||
if (!childId || !isRoomId(childId)) return;
|
||||
|
||||
// because we can not find if a childId is space without joining
|
||||
// or requesting room summary, we will look it into spaceRooms local
|
||||
// cache which we maintain as we load summary in UI.
|
||||
if (getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId)) {
|
||||
const childItem: HierarchyItem = {
|
||||
roomId: childId,
|
||||
content: childEvent.getContent<MSpaceChildContent>(),
|
||||
ts: childEvent.getTs(),
|
||||
space: true,
|
||||
parentId: spaceItem.roomId,
|
||||
};
|
||||
findAndCollectHierarchySpaces(childItem);
|
||||
}
|
||||
});
|
||||
};
|
||||
findAndCollectHierarchySpaces(rootSpaceItem);
|
||||
|
||||
spaceItems = [
|
||||
rootSpaceItem,
|
||||
...spaceItems
|
||||
.filter((item) => item.roomId !== rootSpaceId)
|
||||
.sort(hierarchyItemTs)
|
||||
.sort(hierarchyItemByOrder),
|
||||
];
|
||||
|
||||
return spaceItems;
|
||||
};
|
||||
|
||||
const getSpaceHierarchy = (
|
||||
rootSpaceId: string,
|
||||
spaceRooms: Set<string>,
|
||||
getRoom: (roomId: string) => Room | undefined,
|
||||
closedCategory: (spaceId: string) => boolean
|
||||
): HierarchyItem[] => {
|
||||
const spaceItems: HierarchyItem[] = getHierarchySpaces(rootSpaceId, getRoom, spaceRooms);
|
||||
|
||||
const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
|
||||
const space = getRoom(spaceItem.roomId);
|
||||
if (!space || closedCategory(spaceItem.roomId)) {
|
||||
return [spaceItem];
|
||||
}
|
||||
const childEvents = getStateEvents(space, StateEvent.SpaceChild);
|
||||
const childItems: HierarchyItem[] = [];
|
||||
childEvents.forEach((childEvent) => {
|
||||
if (!isValidChild(childEvent)) return;
|
||||
const childId = childEvent.getStateKey();
|
||||
if (!childId || !isRoomId(childId)) return;
|
||||
if (getRoom(childId)?.isSpaceRoom() || spaceRooms.has(childId)) return;
|
||||
|
||||
const childItem: HierarchyItem = {
|
||||
roomId: childId,
|
||||
content: childEvent.getContent<MSpaceChildContent>(),
|
||||
ts: childEvent.getTs(),
|
||||
parentId: spaceItem.roomId,
|
||||
};
|
||||
childItems.push(childItem);
|
||||
});
|
||||
return [spaceItem, ...childItems.sort(hierarchyItemTs).sort(hierarchyItemByOrder)];
|
||||
});
|
||||
|
||||
return hierarchy;
|
||||
};
|
||||
|
||||
export const useSpaceHierarchy = (
|
||||
spaceId: string,
|
||||
spaceRooms: Set<string>,
|
||||
getRoom: (roomId: string) => Room | undefined,
|
||||
closedCategory: (spaceId: string) => boolean
|
||||
): HierarchyItem[] => {
|
||||
const mx = useMatrixClient();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
|
||||
const [hierarchyAtom] = useState(() =>
|
||||
atom(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory))
|
||||
);
|
||||
const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
|
||||
}, [mx, spaceId, spaceRooms, setHierarchy, getRoom, closedCategory]);
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (mEvent.getType() !== StateEvent.SpaceChild) return;
|
||||
const eventRoomId = mEvent.getRoomId();
|
||||
if (!eventRoomId) return;
|
||||
|
||||
if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) {
|
||||
setHierarchy(getSpaceHierarchy(spaceId, spaceRooms, getRoom, closedCategory));
|
||||
}
|
||||
},
|
||||
[spaceId, roomToParents, setHierarchy, spaceRooms, getRoom, closedCategory]
|
||||
)
|
||||
);
|
||||
|
||||
return hierarchy;
|
||||
};
|
||||
|
||||
const getSpaceJoinedHierarchy = (
|
||||
rootSpaceId: string,
|
||||
getRoom: GetRoomCallback,
|
||||
excludeRoom: (parentId: string, roomId: string) => boolean,
|
||||
sortRoomItems: (parentId: string, items: HierarchyItem[]) => HierarchyItem[]
|
||||
): HierarchyItem[] => {
|
||||
const spaceItems: HierarchyItem[] = getHierarchySpaces(rootSpaceId, getRoom, new Set());
|
||||
|
||||
const hierarchy: HierarchyItem[] = spaceItems.flatMap((spaceItem) => {
|
||||
const space = getRoom(spaceItem.roomId);
|
||||
if (!space) {
|
||||
return [];
|
||||
}
|
||||
const joinedRoomEvents = getStateEvents(space, StateEvent.SpaceChild).filter((childEvent) => {
|
||||
if (!isValidChild(childEvent)) return false;
|
||||
const childId = childEvent.getStateKey();
|
||||
if (!childId || !isRoomId(childId)) return false;
|
||||
const room = getRoom(childId);
|
||||
if (!room || room.isSpaceRoom()) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (joinedRoomEvents.length === 0) return [];
|
||||
|
||||
const childItems: HierarchyItem[] = [];
|
||||
joinedRoomEvents.forEach((childEvent) => {
|
||||
const childId = childEvent.getStateKey();
|
||||
if (!childId) return;
|
||||
|
||||
if (excludeRoom(space.roomId, childId)) return;
|
||||
|
||||
const childItem: HierarchyItem = {
|
||||
roomId: childId,
|
||||
content: childEvent.getContent<MSpaceChildContent>(),
|
||||
ts: childEvent.getTs(),
|
||||
parentId: spaceItem.roomId,
|
||||
};
|
||||
childItems.push(childItem);
|
||||
});
|
||||
return [spaceItem, ...sortRoomItems(spaceItem.roomId, childItems)];
|
||||
});
|
||||
|
||||
return hierarchy;
|
||||
};
|
||||
|
||||
export const useSpaceJoinedHierarchy = (
|
||||
spaceId: string,
|
||||
getRoom: GetRoomCallback,
|
||||
excludeRoom: (parentId: string, roomId: string) => boolean,
|
||||
sortByActivity: (spaceId: string) => boolean
|
||||
): HierarchyItem[] => {
|
||||
const mx = useMatrixClient();
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
|
||||
const sortRoomItems = useCallback(
|
||||
(sId: string, items: HierarchyItem[]) => {
|
||||
if (sortByActivity(sId)) {
|
||||
items.sort((a, b) => factoryRoomIdByActivity(mx)(a.roomId, b.roomId));
|
||||
return items;
|
||||
}
|
||||
items.sort(hierarchyItemTs).sort(hierarchyItemByOrder);
|
||||
return items;
|
||||
},
|
||||
[mx, sortByActivity]
|
||||
);
|
||||
|
||||
const [hierarchyAtom] = useState(() =>
|
||||
atom(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems))
|
||||
);
|
||||
const [hierarchy, setHierarchy] = useAtom(hierarchyAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setHierarchy(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems));
|
||||
}, [mx, spaceId, setHierarchy, getRoom, excludeRoom, sortRoomItems]);
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (mEvent.getType() !== StateEvent.SpaceChild) return;
|
||||
const eventRoomId = mEvent.getRoomId();
|
||||
if (!eventRoomId) return;
|
||||
|
||||
if (spaceId === eventRoomId || getAllParents(roomToParents, eventRoomId).has(spaceId)) {
|
||||
setHierarchy(getSpaceJoinedHierarchy(spaceId, getRoom, excludeRoom, sortRoomItems));
|
||||
}
|
||||
},
|
||||
[spaceId, roomToParents, setHierarchy, getRoom, excludeRoom, sortRoomItems]
|
||||
)
|
||||
);
|
||||
|
||||
return hierarchy;
|
||||
};
|
||||
14
src/app/hooks/useSyncState.ts
Normal file
14
src/app/hooks/useSyncState.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { ClientEvent, ClientEventHandlerMap, MatrixClient } from 'matrix-js-sdk';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useSyncState = (
|
||||
mx: MatrixClient,
|
||||
onChange: ClientEventHandlerMap[ClientEvent.Sync]
|
||||
): void => {
|
||||
useEffect(() => {
|
||||
mx.on(ClientEvent.Sync, onChange);
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.Sync, onChange);
|
||||
};
|
||||
}, [mx, onChange]);
|
||||
};
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { TYPING_TIMEOUT_MS } from '../state/typingMembers';
|
||||
|
||||
type TypingStatusUpdater = (typing: boolean) => void;
|
||||
|
||||
const TYPING_TIMEOUT_MS = 5000; // 5 seconds
|
||||
|
||||
export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): TypingStatusUpdater => {
|
||||
const statusSentTsRef = useRef<number>(0);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue