mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-12 10:10:29 +03:00
Refactor state & Custom editor (#1190)
* Fix eslint * Enable ts strict mode * install folds, jotai & immer * Enable immer map/set * change cross-signing alert anim to 30 iteration * Add function to access matrix client * Add new types * Add disposable util * Add room utils * Add mDirect list atom * Add invite list atom * add room list atom * add utils for jotai atoms * Add room id to parents atom * Add mute list atom * Add room to unread atom * Use hook to bind atoms with sdk * Add settings atom * Add settings hook * Extract set settings hook * Add Sidebar components * WIP * Add bind atoms hook * Fix init muted room list atom * add navigation atoms * Add custom editor * Fix hotkeys * Update folds * Add editor output function * Add matrix client context * Add tooltip to editor toolbar items * WIP - Add editor to room input * Refocus editor on toolbar item click * Add Mentions - WIP * update folds * update mention focus outline * rename emoji element type * Add auto complete menu * add autocomplete query functions * add index file for editor * fix bug in getPrevWord function * Show room mention autocomplete * Add async search function * add use async search hook * use async search in room mention autocomplete * remove folds prefer font for now * allow number array in async search * reset search with empty query * Autocomplete unknown room mention * Autocomplete first room mention on tab * fix roomAliasFromQueryText * change mention color to primary * add isAlive hook * add getMxIdLocalPart to mx utils * fix getRoomAvatarUrl size * fix types * add room members hook * fix bug in room mention * add user mention autocomplete * Fix async search giving prev result after no match * update folds * add twemoji font * add use state provider hook * add prevent scroll with arrow key util * add ts to custom-emoji and emoji files * add types * add hook for emoji group labels * add hook for emoji group icons * add emoji board with basic emoji * add emojiboard in room input * select multiple emoji with shift press * display custom emoji in emojiboard * Add emoji preview * focus element on hover * update folds * position emojiboard properly * convert recent-emoji.js to ts * add use recent emoji hook * add io.element.recent_emoji to account data evt * Render recent emoji in emoji board * show custom emoji from parent spaces * show room emoji * improve emoji sidebar * update folds * fix pack avatar and name fallback in emoji board * add stickers to emoji board * fix bug in emoji preview * Add sticker icon in room input * add debounce hook * add search in emoji board * Optimize emoji board * fix emoji board sidebar divider * sync emojiboard sidebar with scroll & update ui * Add use throttle hook * support custom emoji in editor * remove duplicate emoji selection function * fix emoji and mention spacing * add emoticon autocomplete in editor * fix string * makes emoji size relative to font size in editor * add option to render link element * add spoiler in editor * fix sticker in emoji board search using wrong type * render custom placeholder * update hotkey for block quote and block code * add terminate search function in async search * add getImageInfo to matrix utils * send stickers * add resize observer hook * move emoji board component hooks in hooks dir * prevent editor expand hides room timeline * send typing notifications * improve emoji style and performance * fix imports * add on paste param to editor * add selectFile utils * add file picker hook * add file paste handler hook * add file drop handler * update folds * Add file upload card * add bytes to size util * add blurHash util * add await to js lib * add browser-encrypt-attachment types * add list atom * convert mimetype file to ts * add matrix types * add matrix file util * add file related dom utils * add common utils * add upload atom * add room input draft atom * add upload card renderer component * add upload board component * add support for file upload in editor * send files with message / enter * fix circular deps * store editor toolbar state in local store * move msg content util to separate file * store msg draft on room switch * fix following member not updating on msg sent * add theme for folds component * fix system default theme * Add reply support in editor * prevent initMatrix to init multiple time * add state event hooks * add async callback hook * Show tombstone info for tombstone room * fix room tombstone component border * add power level hook * Add room input placeholder component * Show input placeholder for muted member
This commit is contained in:
parent
2055d7a07f
commit
0b06bed1db
128 changed files with 8799 additions and 409 deletions
63
src/app/state/hooks/inviteList.ts
Normal file
63
src/app/state/hooks/inviteList.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { useAtomValue, WritableAtom } from 'jotai';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { useCallback } from 'react';
|
||||
import { isDirectInvite, isRoom, isSpace, isUnsupportedRoom } from '../../utils/room';
|
||||
import { compareRoomsEqual, RoomsAction } from '../utils';
|
||||
import { MDirectAction } from '../mDirectList';
|
||||
|
||||
export const useSpaceInvites = (
|
||||
mx: MatrixClient,
|
||||
allInvitesAtom: WritableAtom<string[], RoomsAction>
|
||||
) => {
|
||||
const selector = useCallback(
|
||||
(rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))),
|
||||
[mx]
|
||||
);
|
||||
return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
|
||||
};
|
||||
|
||||
export const useRoomInvites = (
|
||||
mx: MatrixClient,
|
||||
allInvitesAtom: WritableAtom<string[], RoomsAction>,
|
||||
mDirectAtom: WritableAtom<Set<string>, MDirectAction>
|
||||
) => {
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const selector = useCallback(
|
||||
(rooms: string[]) =>
|
||||
rooms.filter(
|
||||
(roomId) =>
|
||||
isRoom(mx.getRoom(roomId)) &&
|
||||
!(mDirects.has(roomId) || isDirectInvite(mx.getRoom(roomId), mx.getUserId()))
|
||||
),
|
||||
[mx, mDirects]
|
||||
);
|
||||
return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
|
||||
};
|
||||
|
||||
export const useDirectInvites = (
|
||||
mx: MatrixClient,
|
||||
allInvitesAtom: WritableAtom<string[], RoomsAction>,
|
||||
mDirectAtom: WritableAtom<Set<string>, MDirectAction>
|
||||
) => {
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const selector = useCallback(
|
||||
(rooms: string[]) =>
|
||||
rooms.filter(
|
||||
(roomId) => mDirects.has(roomId) || isDirectInvite(mx.getRoom(roomId), mx.getUserId())
|
||||
),
|
||||
[mx, mDirects]
|
||||
);
|
||||
return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
|
||||
};
|
||||
|
||||
export const useUnsupportedInvites = (
|
||||
mx: MatrixClient,
|
||||
allInvitesAtom: WritableAtom<string[], RoomsAction>
|
||||
) => {
|
||||
const selector = useCallback(
|
||||
(rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))),
|
||||
[mx]
|
||||
);
|
||||
return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
|
||||
};
|
||||
54
src/app/state/hooks/roomList.ts
Normal file
54
src/app/state/hooks/roomList.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { useAtomValue, WritableAtom } from 'jotai';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { useCallback } from 'react';
|
||||
import { isRoom, isSpace, isUnsupportedRoom } from '../../utils/room';
|
||||
import { compareRoomsEqual, RoomsAction } from '../utils';
|
||||
import { MDirectAction } from '../mDirectList';
|
||||
|
||||
export const useSpaces = (mx: MatrixClient, allRoomsAtom: WritableAtom<string[], RoomsAction>) => {
|
||||
const selector = useCallback(
|
||||
(rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))),
|
||||
[mx]
|
||||
);
|
||||
return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual));
|
||||
};
|
||||
|
||||
export const useRooms = (
|
||||
mx: MatrixClient,
|
||||
allRoomsAtom: WritableAtom<string[], RoomsAction>,
|
||||
mDirectAtom: WritableAtom<Set<string>, MDirectAction>
|
||||
) => {
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const selector = useCallback(
|
||||
(rooms: string[]) =>
|
||||
rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && !mDirects.has(roomId)),
|
||||
[mx, mDirects]
|
||||
);
|
||||
return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual));
|
||||
};
|
||||
|
||||
export const useDirects = (
|
||||
mx: MatrixClient,
|
||||
allRoomsAtom: WritableAtom<string[], RoomsAction>,
|
||||
mDirectAtom: WritableAtom<Set<string>, MDirectAction>
|
||||
) => {
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const selector = useCallback(
|
||||
(rooms: string[]) =>
|
||||
rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && mDirects.has(roomId)),
|
||||
[mx, mDirects]
|
||||
);
|
||||
return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual));
|
||||
};
|
||||
|
||||
export const useUnsupportedRooms = (
|
||||
mx: MatrixClient,
|
||||
allRoomsAtom: WritableAtom<string[], RoomsAction>
|
||||
) => {
|
||||
const selector = useCallback(
|
||||
(rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))),
|
||||
[mx]
|
||||
);
|
||||
return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual));
|
||||
};
|
||||
34
src/app/state/hooks/settings.ts
Normal file
34
src/app/state/hooks/settings.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { atom, useAtomValue, useSetAtom, WritableAtom } from 'jotai';
|
||||
import { SetAtom } from 'jotai/core/atom';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { Settings } from '../settings';
|
||||
|
||||
export const useSetSetting = <K extends keyof Settings>(
|
||||
settingsAtom: WritableAtom<Settings, Settings>,
|
||||
key: K
|
||||
) => {
|
||||
const setterAtom = useMemo(
|
||||
() =>
|
||||
atom<null, Settings[K]>(null, (get, set, value) => {
|
||||
const s = { ...get(settingsAtom) };
|
||||
s[key] = value;
|
||||
set(settingsAtom, s);
|
||||
}),
|
||||
[settingsAtom, key]
|
||||
);
|
||||
|
||||
return useSetAtom(setterAtom);
|
||||
};
|
||||
|
||||
export const useSetting = <K extends keyof Settings>(
|
||||
settingsAtom: WritableAtom<Settings, Settings>,
|
||||
key: K
|
||||
): [Settings[K], SetAtom<Settings[K], void>] => {
|
||||
const selector = useMemo(() => (s: Settings) => s[key], [key]);
|
||||
const setting = useAtomValue(selectAtom(settingsAtom, selector));
|
||||
|
||||
const setter = useSetSetting(settingsAtom, key);
|
||||
|
||||
return [setting, setter];
|
||||
};
|
||||
16
src/app/state/hooks/useBindAtoms.ts
Normal file
16
src/app/state/hooks/useBindAtoms.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { allInvitesAtom, useBindAllInvitesAtom } from '../inviteList';
|
||||
import { allRoomsAtom, useBindAllRoomsAtom } from '../roomList';
|
||||
import { mDirectAtom, useBindMDirectAtom } from '../mDirectList';
|
||||
import { muteChangesAtom, mutedRoomsAtom, useBindMutedRoomsAtom } from '../mutedRoomList';
|
||||
import { roomToUnreadAtom, useBindRoomToUnreadAtom } from '../roomToUnread';
|
||||
import { roomToParentsAtom, useBindRoomToParentsAtom } from '../roomToParents';
|
||||
|
||||
export const useBindAtoms = (mx: MatrixClient) => {
|
||||
useBindMDirectAtom(mx, mDirectAtom);
|
||||
useBindAllInvitesAtom(mx, allInvitesAtom);
|
||||
useBindAllRoomsAtom(mx, allRoomsAtom);
|
||||
useBindRoomToParentsAtom(mx, roomToParentsAtom);
|
||||
useBindMutedRoomsAtom(mx, mutedRoomsAtom);
|
||||
useBindRoomToUnreadAtom(mx, roomToUnreadAtom, muteChangesAtom);
|
||||
};
|
||||
32
src/app/state/inviteList.ts
Normal file
32
src/app/state/inviteList.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { atom, WritableAtom } from 'jotai';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { useMemo } from 'react';
|
||||
import { Membership } from '../../types/matrix/room';
|
||||
import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils';
|
||||
|
||||
const baseRoomsAtom = atom<string[]>([]);
|
||||
export const allInvitesAtom = atom<string[], RoomsAction>(
|
||||
(get) => get(baseRoomsAtom),
|
||||
(get, set, action) => {
|
||||
if (action.type === 'INITIALIZE') {
|
||||
set(baseRoomsAtom, action.rooms);
|
||||
return;
|
||||
}
|
||||
set(baseRoomsAtom, (ids) => {
|
||||
const newIds = ids.filter((id) => id !== action.roomId);
|
||||
if (action.type === 'PUT') newIds.push(action.roomId);
|
||||
return newIds;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const useBindAllInvitesAtom = (
|
||||
mx: MatrixClient,
|
||||
allRooms: WritableAtom<string[], RoomsAction>
|
||||
) => {
|
||||
useBindRoomsWithMembershipsAtom(
|
||||
mx,
|
||||
allRooms,
|
||||
useMemo(() => [Membership.Invite], [])
|
||||
);
|
||||
};
|
||||
33
src/app/state/list.ts
Normal file
33
src/app/state/list.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
export type ListAction<T> =
|
||||
| {
|
||||
type: 'PUT';
|
||||
item: T | T[];
|
||||
}
|
||||
| {
|
||||
type: 'DELETE';
|
||||
item: T | T[];
|
||||
};
|
||||
|
||||
export const createListAtom = <T>() => {
|
||||
const baseListAtom = atom<T[]>([]);
|
||||
return atom<T[], ListAction<T>>(
|
||||
(get) => get(baseListAtom),
|
||||
(get, set, action) => {
|
||||
const items = get(baseListAtom);
|
||||
const newItems = Array.isArray(action.item) ? action.item : [action.item];
|
||||
if (action.type === 'DELETE') {
|
||||
set(
|
||||
baseListAtom,
|
||||
items.filter((item) => !newItems.includes(item))
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (action.type === 'PUT') {
|
||||
set(baseListAtom, [...items, ...newItems]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
export type TListAtom<T> = ReturnType<typeof createListAtom<T>>;
|
||||
47
src/app/state/mDirectList.ts
Normal file
47
src/app/state/mDirectList.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { atom, useSetAtom, WritableAtom } from 'jotai';
|
||||
import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
|
||||
import { useEffect } from 'react';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import { getAccountData, getMDirects } from '../utils/room';
|
||||
|
||||
export type MDirectAction = {
|
||||
type: 'INITIALIZE' | 'UPDATE';
|
||||
rooms: Set<string>;
|
||||
};
|
||||
|
||||
const baseMDirectAtom = atom(new Set<string>());
|
||||
export const mDirectAtom = atom<Set<string>, MDirectAction>(
|
||||
(get) => get(baseMDirectAtom),
|
||||
(get, set, action) => {
|
||||
set(baseMDirectAtom, action.rooms);
|
||||
}
|
||||
);
|
||||
|
||||
export const useBindMDirectAtom = (
|
||||
mx: MatrixClient,
|
||||
mDirect: WritableAtom<Set<string>, MDirectAction>
|
||||
) => {
|
||||
const setMDirect = useSetAtom(mDirect);
|
||||
|
||||
useEffect(() => {
|
||||
const mDirectEvent = getAccountData(mx, AccountDataEvent.Direct);
|
||||
if (mDirectEvent) {
|
||||
setMDirect({
|
||||
type: 'INITIALIZE',
|
||||
rooms: getMDirects(mDirectEvent),
|
||||
});
|
||||
}
|
||||
|
||||
const handleAccountData = (event: MatrixEvent) => {
|
||||
setMDirect({
|
||||
type: 'UPDATE',
|
||||
rooms: getMDirects(event),
|
||||
});
|
||||
};
|
||||
|
||||
mx.on(ClientEvent.AccountData, handleAccountData);
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.AccountData, handleAccountData);
|
||||
};
|
||||
}, [mx, setMDirect]);
|
||||
};
|
||||
101
src/app/state/mutedRoomList.ts
Normal file
101
src/app/state/mutedRoomList.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { atom, WritableAtom, useSetAtom } from 'jotai';
|
||||
import { ClientEvent, IPushRule, IPushRules, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
|
||||
import { useEffect } from 'react';
|
||||
import { MuteChanges } from '../../types/matrix/room';
|
||||
import { findMutedRule, isMutedRule } from '../utils/room';
|
||||
|
||||
export type MutedRoomsUpdate =
|
||||
| {
|
||||
type: 'INITIALIZE';
|
||||
addRooms: string[];
|
||||
}
|
||||
| {
|
||||
type: 'UPDATE';
|
||||
addRooms: string[];
|
||||
removeRooms: string[];
|
||||
};
|
||||
|
||||
export const muteChangesAtom = atom<MuteChanges>({
|
||||
added: [],
|
||||
removed: [],
|
||||
});
|
||||
|
||||
const baseMutedRoomsAtom = atom(new Set<string>());
|
||||
export const mutedRoomsAtom = atom<Set<string>, MutedRoomsUpdate>(
|
||||
(get) => get(baseMutedRoomsAtom),
|
||||
(get, set, action) => {
|
||||
const mutedRooms = new Set([...get(mutedRoomsAtom)]);
|
||||
if (action.type === 'INITIALIZE') {
|
||||
set(baseMutedRoomsAtom, new Set([...action.addRooms]));
|
||||
set(muteChangesAtom, {
|
||||
added: [...action.addRooms],
|
||||
removed: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action.type === 'UPDATE') {
|
||||
action.removeRooms.forEach((roomId) => mutedRooms.delete(roomId));
|
||||
action.addRooms.forEach((roomId) => mutedRooms.add(roomId));
|
||||
set(baseMutedRoomsAtom, mutedRooms);
|
||||
set(muteChangesAtom, {
|
||||
added: [...action.addRooms],
|
||||
removed: [...action.removeRooms],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const useBindMutedRoomsAtom = (
|
||||
mx: MatrixClient,
|
||||
mutedAtom: WritableAtom<Set<string>, MutedRoomsUpdate>
|
||||
) => {
|
||||
const setMuted = useSetAtom(mutedAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const overrideRules = mx.getAccountData('m.push_rules')?.getContent<IPushRules>()
|
||||
?.global?.override;
|
||||
if (overrideRules) {
|
||||
const mutedRooms = overrideRules.reduce<string[]>((rooms, rule) => {
|
||||
if (isMutedRule(rule)) rooms.push(rule.rule_id);
|
||||
return rooms;
|
||||
}, []);
|
||||
setMuted({
|
||||
type: 'INITIALIZE',
|
||||
addRooms: mutedRooms,
|
||||
});
|
||||
}
|
||||
}, [mx, setMuted]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePushRules = (mEvent: MatrixEvent, oldMEvent?: MatrixEvent) => {
|
||||
if (mEvent.getType() === 'm.push_rules') {
|
||||
const override = mEvent?.getContent()?.global?.override as IPushRule[] | undefined;
|
||||
const oldOverride = oldMEvent?.getContent()?.global?.override as IPushRule[] | undefined;
|
||||
if (!override || !oldOverride) return;
|
||||
|
||||
const isMuteToggled = (rule: IPushRule, otherOverride: IPushRule[]) => {
|
||||
const roomId = rule.rule_id;
|
||||
|
||||
const isMuted = isMutedRule(rule);
|
||||
if (!isMuted) return false;
|
||||
const isOtherMuted = findMutedRule(otherOverride, roomId);
|
||||
if (isOtherMuted) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const mutedRules = override.filter((rule) => isMuteToggled(rule, oldOverride));
|
||||
const unMutedRules = oldOverride.filter((rule) => isMuteToggled(rule, override));
|
||||
|
||||
setMuted({
|
||||
type: 'UPDATE',
|
||||
addRooms: mutedRules.map((rule) => rule.rule_id),
|
||||
removeRooms: unMutedRules.map((rule) => rule.rule_id),
|
||||
});
|
||||
}
|
||||
};
|
||||
mx.on(ClientEvent.AccountData, handlePushRules);
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.AccountData, handlePushRules);
|
||||
};
|
||||
}, [mx, setMuted]);
|
||||
};
|
||||
48
src/app/state/roomInputDrafts.ts
Normal file
48
src/app/state/roomInputDrafts.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { atom } from 'jotai';
|
||||
import { atomFamily } from 'jotai/utils';
|
||||
import { Descendant } from 'slate';
|
||||
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
||||
import { TListAtom, createListAtom } from './list';
|
||||
import { createUploadAtomFamily } from './upload';
|
||||
import { TUploadContent } from '../utils/matrix';
|
||||
|
||||
export const roomUploadAtomFamily = createUploadAtomFamily();
|
||||
|
||||
export type TUploadItem = {
|
||||
file: TUploadContent;
|
||||
originalFile: TUploadContent;
|
||||
encInfo: EncryptedAttachmentInfo | undefined;
|
||||
};
|
||||
|
||||
export const roomIdToUploadItemsAtomFamily = atomFamily<string, TListAtom<TUploadItem>>(
|
||||
createListAtom
|
||||
);
|
||||
|
||||
export type RoomIdToMsgAction =
|
||||
| {
|
||||
type: 'PUT';
|
||||
roomId: string;
|
||||
msg: Descendant[];
|
||||
}
|
||||
| {
|
||||
type: 'DELETE';
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
const createMsgDraftAtom = () => atom<Descendant[]>([]);
|
||||
export type TMsgDraftAtom = ReturnType<typeof createMsgDraftAtom>;
|
||||
export const roomIdToMsgDraftAtomFamily = atomFamily<string, TMsgDraftAtom>(() =>
|
||||
createMsgDraftAtom()
|
||||
);
|
||||
|
||||
export type IReplyDraft = {
|
||||
userId: string;
|
||||
eventId: string;
|
||||
body: string;
|
||||
formattedBody?: string;
|
||||
};
|
||||
const createReplyDraftAtom = () => atom<IReplyDraft | undefined>(undefined);
|
||||
export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>;
|
||||
export const roomIdToReplyDraftAtomFamily = atomFamily<string, TReplyDraftAtom>(() =>
|
||||
createReplyDraftAtom()
|
||||
);
|
||||
31
src/app/state/roomList.ts
Normal file
31
src/app/state/roomList.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { atom, WritableAtom } from 'jotai';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { useMemo } from 'react';
|
||||
import { Membership } from '../../types/matrix/room';
|
||||
import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils';
|
||||
|
||||
const baseRoomsAtom = atom<string[]>([]);
|
||||
export const allRoomsAtom = atom<string[], RoomsAction>(
|
||||
(get) => get(baseRoomsAtom),
|
||||
(get, set, action) => {
|
||||
if (action.type === 'INITIALIZE') {
|
||||
set(baseRoomsAtom, action.rooms);
|
||||
return;
|
||||
}
|
||||
set(baseRoomsAtom, (ids) => {
|
||||
const newIds = ids.filter((id) => id !== action.roomId);
|
||||
if (action.type === 'PUT') newIds.push(action.roomId);
|
||||
return newIds;
|
||||
});
|
||||
}
|
||||
);
|
||||
export const useBindAllRoomsAtom = (
|
||||
mx: MatrixClient,
|
||||
allRooms: WritableAtom<string[], RoomsAction>
|
||||
) => {
|
||||
useBindRoomsWithMembershipsAtom(
|
||||
mx,
|
||||
allRooms,
|
||||
useMemo(() => [Membership.Join], [])
|
||||
);
|
||||
};
|
||||
120
src/app/state/roomToParents.ts
Normal file
120
src/app/state/roomToParents.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import produce from 'immer';
|
||||
import { atom, useSetAtom, WritableAtom } from 'jotai';
|
||||
import {
|
||||
ClientEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomEvent,
|
||||
RoomStateEvent,
|
||||
} from 'matrix-js-sdk';
|
||||
import { useEffect } from 'react';
|
||||
import { Membership, RoomToParents, StateEvent } from '../../types/matrix/room';
|
||||
import {
|
||||
getRoomToParents,
|
||||
getSpaceChildren,
|
||||
isSpace,
|
||||
isValidChild,
|
||||
mapParentWithChildren,
|
||||
} from '../utils/room';
|
||||
|
||||
export type RoomToParentsAction =
|
||||
| {
|
||||
type: 'INITIALIZE';
|
||||
roomToParents: RoomToParents;
|
||||
}
|
||||
| {
|
||||
type: 'PUT';
|
||||
parent: string;
|
||||
children: string[];
|
||||
}
|
||||
| {
|
||||
type: 'DELETE';
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
const baseRoomToParents = atom<RoomToParents>(new Map());
|
||||
export const roomToParentsAtom = atom<RoomToParents, RoomToParentsAction>(
|
||||
(get) => get(baseRoomToParents),
|
||||
(get, set, action) => {
|
||||
if (action.type === 'INITIALIZE') {
|
||||
set(baseRoomToParents, action.roomToParents);
|
||||
return;
|
||||
}
|
||||
if (action.type === 'PUT') {
|
||||
set(
|
||||
baseRoomToParents,
|
||||
produce(get(baseRoomToParents), (draftRoomToParents) => {
|
||||
mapParentWithChildren(draftRoomToParents, action.parent, action.children);
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (action.type === 'DELETE') {
|
||||
set(
|
||||
baseRoomToParents,
|
||||
produce(get(baseRoomToParents), (draftRoomToParents) => {
|
||||
const noParentRooms: string[] = [];
|
||||
draftRoomToParents.delete(action.roomId);
|
||||
draftRoomToParents.forEach((parents, child) => {
|
||||
parents.delete(action.roomId);
|
||||
if (parents.size === 0) noParentRooms.push(child);
|
||||
});
|
||||
noParentRooms.forEach((room) => draftRoomToParents.delete(room));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const useBindRoomToParentsAtom = (
|
||||
mx: MatrixClient,
|
||||
roomToParents: WritableAtom<RoomToParents, RoomToParentsAction>
|
||||
) => {
|
||||
const setRoomToParents = useSetAtom(roomToParents);
|
||||
|
||||
useEffect(() => {
|
||||
setRoomToParents({ type: 'INITIALIZE', roomToParents: getRoomToParents(mx) });
|
||||
|
||||
const handleAddRoom = (room: Room) => {
|
||||
if (isSpace(room) && room.getMyMembership() !== Membership.Invite) {
|
||||
setRoomToParents({ type: 'PUT', parent: room.roomId, children: getSpaceChildren(room) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMembershipChange = (room: Room, membership: string) => {
|
||||
if (isSpace(room) && membership === Membership.Join) {
|
||||
setRoomToParents({ type: 'PUT', parent: room.roomId, children: getSpaceChildren(room) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleStateChange = (mEvent: MatrixEvent) => {
|
||||
if (mEvent.getType() === StateEvent.SpaceChild) {
|
||||
const childId = mEvent.getStateKey();
|
||||
const roomId = mEvent.getRoomId();
|
||||
if (childId && roomId) {
|
||||
if (isValidChild(mEvent)) {
|
||||
setRoomToParents({ type: 'PUT', parent: roomId, children: [childId] });
|
||||
} else {
|
||||
setRoomToParents({ type: 'DELETE', roomId: childId });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRoom = (roomId: string) => {
|
||||
setRoomToParents({ type: 'DELETE', roomId });
|
||||
};
|
||||
|
||||
mx.on(ClientEvent.Room, handleAddRoom);
|
||||
mx.on(RoomEvent.MyMembership, handleMembershipChange);
|
||||
mx.on(RoomStateEvent.Events, handleStateChange);
|
||||
mx.on(ClientEvent.DeleteRoom, handleDeleteRoom);
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.Room, handleAddRoom);
|
||||
mx.removeListener(RoomEvent.MyMembership, handleMembershipChange);
|
||||
mx.removeListener(RoomStateEvent.Events, handleStateChange);
|
||||
mx.removeListener(ClientEvent.DeleteRoom, handleDeleteRoom);
|
||||
};
|
||||
}, [mx, setRoomToParents]);
|
||||
};
|
||||
219
src/app/state/roomToUnread.ts
Normal file
219
src/app/state/roomToUnread.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
import produce from 'immer';
|
||||
import { atom, useSetAtom, PrimitiveAtom, WritableAtom, useAtomValue } from 'jotai';
|
||||
import { IRoomTimelineData, MatrixClient, MatrixEvent, Room, RoomEvent } from 'matrix-js-sdk';
|
||||
import { ReceiptContent, ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
MuteChanges,
|
||||
Membership,
|
||||
NotificationType,
|
||||
RoomToUnread,
|
||||
UnreadInfo,
|
||||
} from '../../types/matrix/room';
|
||||
import {
|
||||
getAllParents,
|
||||
getNotificationType,
|
||||
getUnreadInfo,
|
||||
getUnreadInfos,
|
||||
isNotificationEvent,
|
||||
roomHaveUnread,
|
||||
} from '../utils/room';
|
||||
import { roomToParentsAtom } from './roomToParents';
|
||||
|
||||
export type RoomToUnreadAction =
|
||||
| {
|
||||
type: 'RESET';
|
||||
unreadInfos: UnreadInfo[];
|
||||
}
|
||||
| {
|
||||
type: 'PUT';
|
||||
unreadInfo: UnreadInfo;
|
||||
}
|
||||
| {
|
||||
type: 'DELETE';
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
const putUnreadInfo = (
|
||||
roomToUnread: RoomToUnread,
|
||||
allParents: Set<string>,
|
||||
unreadInfo: UnreadInfo
|
||||
) => {
|
||||
const oldUnread = roomToUnread.get(unreadInfo.roomId) ?? { highlight: 0, total: 0, from: null };
|
||||
roomToUnread.set(unreadInfo.roomId, {
|
||||
highlight: unreadInfo.highlight,
|
||||
total: unreadInfo.total,
|
||||
from: null,
|
||||
});
|
||||
|
||||
const newH = unreadInfo.highlight - oldUnread.highlight;
|
||||
const newT = unreadInfo.total - oldUnread.total;
|
||||
|
||||
allParents.forEach((parentId) => {
|
||||
const oldParentUnread = roomToUnread.get(parentId) ?? { highlight: 0, total: 0, from: null };
|
||||
roomToUnread.set(parentId, {
|
||||
highlight: (oldParentUnread.highlight += newH),
|
||||
total: (oldParentUnread.total += newT),
|
||||
from: new Set([...(oldParentUnread.from ?? []), unreadInfo.roomId]),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUnreadInfo = (roomToUnread: RoomToUnread, allParents: Set<string>, roomId: string) => {
|
||||
const oldUnread = roomToUnread.get(roomId);
|
||||
if (!oldUnread) return;
|
||||
roomToUnread.delete(roomId);
|
||||
|
||||
allParents.forEach((parentId) => {
|
||||
const oldParentUnread = roomToUnread.get(parentId);
|
||||
if (!oldParentUnread) return;
|
||||
const newFrom = new Set([...(oldParentUnread.from ?? roomId)]);
|
||||
newFrom.delete(roomId);
|
||||
if (newFrom.size === 0) {
|
||||
roomToUnread.delete(parentId);
|
||||
return;
|
||||
}
|
||||
roomToUnread.set(parentId, {
|
||||
highlight: oldParentUnread.highlight - oldUnread.highlight,
|
||||
total: oldParentUnread.total - oldUnread.total,
|
||||
from: newFrom,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const baseRoomToUnread = atom<RoomToUnread>(new Map());
|
||||
export const roomToUnreadAtom = atom<RoomToUnread, RoomToUnreadAction>(
|
||||
(get) => get(baseRoomToUnread),
|
||||
(get, set, action) => {
|
||||
if (action.type === 'RESET') {
|
||||
const draftRoomToUnread: RoomToUnread = new Map();
|
||||
action.unreadInfos.forEach((unreadInfo) => {
|
||||
putUnreadInfo(
|
||||
draftRoomToUnread,
|
||||
getAllParents(get(roomToParentsAtom), unreadInfo.roomId),
|
||||
unreadInfo
|
||||
);
|
||||
});
|
||||
set(baseRoomToUnread, draftRoomToUnread);
|
||||
return;
|
||||
}
|
||||
if (action.type === 'PUT') {
|
||||
set(
|
||||
baseRoomToUnread,
|
||||
produce(get(baseRoomToUnread), (draftRoomToUnread) =>
|
||||
putUnreadInfo(
|
||||
draftRoomToUnread,
|
||||
getAllParents(get(roomToParentsAtom), action.unreadInfo.roomId),
|
||||
action.unreadInfo
|
||||
)
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (action.type === 'DELETE' && get(baseRoomToUnread).has(action.roomId)) {
|
||||
set(
|
||||
baseRoomToUnread,
|
||||
produce(get(baseRoomToUnread), (draftRoomToUnread) =>
|
||||
deleteUnreadInfo(
|
||||
draftRoomToUnread,
|
||||
getAllParents(get(roomToParentsAtom), action.roomId),
|
||||
action.roomId
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const useBindRoomToUnreadAtom = (
|
||||
mx: MatrixClient,
|
||||
unreadAtom: WritableAtom<RoomToUnread, RoomToUnreadAction>,
|
||||
muteChangesAtom: PrimitiveAtom<MuteChanges>
|
||||
) => {
|
||||
const setUnreadAtom = useSetAtom(unreadAtom);
|
||||
const muteChanges = useAtomValue(muteChangesAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setUnreadAtom({
|
||||
type: 'RESET',
|
||||
unreadInfos: getUnreadInfos(mx),
|
||||
});
|
||||
}, [mx, setUnreadAtom]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleTimelineEvent = (
|
||||
mEvent: MatrixEvent,
|
||||
room: Room | undefined,
|
||||
toStartOfTimeline: boolean | undefined,
|
||||
removed: boolean,
|
||||
data: IRoomTimelineData
|
||||
) => {
|
||||
if (!room || !data.liveEvent || room.isSpaceRoom() || !isNotificationEvent(mEvent)) return;
|
||||
if (getNotificationType(mx, room.roomId) === NotificationType.Mute) {
|
||||
setUnreadAtom({
|
||||
type: 'DELETE',
|
||||
roomId: room.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (mEvent.getSender() === mx.getUserId()) return;
|
||||
setUnreadAtom({ type: 'PUT', unreadInfo: getUnreadInfo(room) });
|
||||
};
|
||||
mx.on(RoomEvent.Timeline, handleTimelineEvent);
|
||||
return () => {
|
||||
mx.removeListener(RoomEvent.Timeline, handleTimelineEvent);
|
||||
};
|
||||
}, [mx, setUnreadAtom]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleReceipt = (mEvent: MatrixEvent, room: Room) => {
|
||||
if (mEvent.getType() === 'm.receipt') {
|
||||
const myUserId = mx.getUserId();
|
||||
if (!myUserId) return;
|
||||
if (room.isSpaceRoom()) return;
|
||||
const content = mEvent.getContent<ReceiptContent>();
|
||||
|
||||
const isMyReceipt = Object.keys(content).find((eventId) =>
|
||||
(Object.keys(content[eventId]) as ReceiptType[]).find(
|
||||
(receiptType) => content[eventId][receiptType][myUserId]
|
||||
)
|
||||
);
|
||||
if (isMyReceipt) {
|
||||
setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
|
||||
}
|
||||
}
|
||||
};
|
||||
mx.on(RoomEvent.Receipt, handleReceipt);
|
||||
return () => {
|
||||
mx.removeListener(RoomEvent.Receipt, handleReceipt);
|
||||
};
|
||||
}, [mx, setUnreadAtom]);
|
||||
|
||||
useEffect(() => {
|
||||
muteChanges.removed.forEach((roomId) => {
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return;
|
||||
if (!roomHaveUnread(mx, room)) return;
|
||||
setUnreadAtom({ type: 'PUT', unreadInfo: getUnreadInfo(room) });
|
||||
});
|
||||
muteChanges.added.forEach((roomId) => {
|
||||
setUnreadAtom({ type: 'DELETE', roomId });
|
||||
});
|
||||
}, [mx, setUnreadAtom, muteChanges]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMembershipChange = (room: Room, membership: string) => {
|
||||
if (membership !== Membership.Join) {
|
||||
setUnreadAtom({
|
||||
type: 'DELETE',
|
||||
roomId: room.roomId,
|
||||
});
|
||||
}
|
||||
};
|
||||
mx.on(RoomEvent.MyMembership, handleMembershipChange);
|
||||
return () => {
|
||||
mx.removeListener(RoomEvent.MyMembership, handleMembershipChange);
|
||||
};
|
||||
}, [mx, setUnreadAtom]);
|
||||
};
|
||||
3
src/app/state/selectedRoom.ts
Normal file
3
src/app/state/selectedRoom.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
export const selectedRoomAtom = atom<string | undefined>(undefined);
|
||||
8
src/app/state/selectedTab.ts
Normal file
8
src/app/state/selectedTab.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
export enum SidebarTab {
|
||||
Home = 'Home',
|
||||
People = 'People',
|
||||
}
|
||||
|
||||
export const selectedTabAtom = atom<SidebarTab | string>(SidebarTab.Home);
|
||||
49
src/app/state/settings.ts
Normal file
49
src/app/state/settings.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { atom } from 'jotai';
|
||||
|
||||
const STORAGE_KEY = 'settings';
|
||||
export interface Settings {
|
||||
themeIndex: number;
|
||||
useSystemTheme: boolean;
|
||||
isMarkdown: boolean;
|
||||
editorToolbar: boolean;
|
||||
isPeopleDrawer: boolean;
|
||||
|
||||
hideMembershipEvents: boolean;
|
||||
hideNickAvatarEvents: boolean;
|
||||
|
||||
showNotifications: boolean;
|
||||
isNotificationSounds: boolean;
|
||||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
themeIndex: 0,
|
||||
useSystemTheme: true,
|
||||
isMarkdown: true,
|
||||
editorToolbar: false,
|
||||
isPeopleDrawer: true,
|
||||
|
||||
hideMembershipEvents: false,
|
||||
hideNickAvatarEvents: true,
|
||||
|
||||
showNotifications: true,
|
||||
isNotificationSounds: true,
|
||||
};
|
||||
|
||||
export const getSettings = () => {
|
||||
const settings = localStorage.getItem(STORAGE_KEY);
|
||||
if (settings === null) return defaultSettings;
|
||||
return JSON.parse(settings) as Settings;
|
||||
};
|
||||
|
||||
export const setSettings = (settings: Settings) => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
||||
};
|
||||
|
||||
const baseSettings = atom<Settings>(getSettings());
|
||||
export const settingsAtom = atom<Settings, Settings>(
|
||||
(get) => get(baseSettings),
|
||||
(get, set, update) => {
|
||||
set(baseSettings, update);
|
||||
setSettings(update);
|
||||
}
|
||||
);
|
||||
34
src/app/state/tabToRoom.ts
Normal file
34
src/app/state/tabToRoom.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import produce from 'immer';
|
||||
import { atom } from 'jotai';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
type RoomInfo = {
|
||||
roomId: string;
|
||||
timestamp: number;
|
||||
};
|
||||
type TabToRoom = Map<string, RoomInfo>;
|
||||
|
||||
type TabToRoomAction = {
|
||||
type: 'PUT';
|
||||
tabInfo: { tabId: string; roomInfo: RoomInfo };
|
||||
};
|
||||
|
||||
const baseTabToRoom = atom<TabToRoom>(new Map());
|
||||
export const tabToRoomAtom = atom<TabToRoom, TabToRoomAction>(
|
||||
(get) => get(baseTabToRoom),
|
||||
(get, set, action) => {
|
||||
if (action.type === 'PUT') {
|
||||
set(
|
||||
baseTabToRoom,
|
||||
produce(get(baseTabToRoom), (draft) => {
|
||||
draft.set(action.tabInfo.tabId, action.tabInfo.roomInfo);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const useBindTabToRoomAtom = (mx: MatrixClient) => {
|
||||
console.log(mx);
|
||||
// TODO:
|
||||
};
|
||||
146
src/app/state/upload.ts
Normal file
146
src/app/state/upload.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { atom, useAtom } from 'jotai';
|
||||
import { atomFamily } from 'jotai/utils';
|
||||
import { MatrixClient, UploadResponse, UploadProgress, MatrixError } from 'matrix-js-sdk';
|
||||
import { useCallback } from 'react';
|
||||
import { useThrottle } from '../hooks/useThrottle';
|
||||
import { uploadContent, TUploadContent } from '../utils/matrix';
|
||||
|
||||
export enum UploadStatus {
|
||||
Idle = 'idle',
|
||||
Loading = 'loading',
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type UploadIdle = {
|
||||
file: TUploadContent;
|
||||
status: UploadStatus.Idle;
|
||||
};
|
||||
|
||||
export type UploadLoading = {
|
||||
file: TUploadContent;
|
||||
status: UploadStatus.Loading;
|
||||
promise: Promise<UploadResponse>;
|
||||
progress: UploadProgress;
|
||||
};
|
||||
|
||||
export type UploadSuccess = {
|
||||
file: TUploadContent;
|
||||
status: UploadStatus.Success;
|
||||
mxc: string;
|
||||
};
|
||||
|
||||
export type UploadError = {
|
||||
file: TUploadContent;
|
||||
status: UploadStatus.Error;
|
||||
error: MatrixError;
|
||||
};
|
||||
|
||||
export type Upload = UploadIdle | UploadLoading | UploadSuccess | UploadError;
|
||||
|
||||
export type UploadAtomAction =
|
||||
| {
|
||||
promise: Promise<UploadResponse>;
|
||||
}
|
||||
| {
|
||||
progress: UploadProgress;
|
||||
}
|
||||
| {
|
||||
mxc: string;
|
||||
}
|
||||
| {
|
||||
error: MatrixError;
|
||||
};
|
||||
|
||||
export const createUploadAtom = (file: TUploadContent) => {
|
||||
const baseUploadAtom = atom<Upload>({
|
||||
file,
|
||||
status: UploadStatus.Idle,
|
||||
});
|
||||
return atom<Upload, UploadAtomAction>(
|
||||
(get) => get(baseUploadAtom),
|
||||
(get, set, update) => {
|
||||
const uploadState = get(baseUploadAtom);
|
||||
if ('promise' in update) {
|
||||
set(baseUploadAtom, {
|
||||
status: UploadStatus.Loading,
|
||||
file,
|
||||
promise: update.promise,
|
||||
progress: { loaded: 0, total: file.size },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ('progress' in update && uploadState.status === UploadStatus.Loading) {
|
||||
set(baseUploadAtom, {
|
||||
...uploadState,
|
||||
progress: update.progress,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ('mxc' in update) {
|
||||
set(baseUploadAtom, {
|
||||
status: UploadStatus.Success,
|
||||
file,
|
||||
mxc: update.mxc,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ('error' in update) {
|
||||
set(baseUploadAtom, {
|
||||
status: UploadStatus.Error,
|
||||
file,
|
||||
error: update.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
export type TUploadAtom = ReturnType<typeof createUploadAtom>;
|
||||
|
||||
export const useBindUploadAtom = (
|
||||
mx: MatrixClient,
|
||||
file: TUploadContent,
|
||||
uploadAtom: TUploadAtom,
|
||||
hideFilename?: boolean
|
||||
) => {
|
||||
const [upload, setUpload] = useAtom(uploadAtom);
|
||||
|
||||
const handleProgress = useThrottle(
|
||||
useCallback((progress: UploadProgress) => setUpload({ progress }), [setUpload]),
|
||||
{ immediate: true, wait: 200 }
|
||||
);
|
||||
|
||||
const startUpload = useCallback(
|
||||
() =>
|
||||
uploadContent(mx, file, {
|
||||
hideFilename,
|
||||
onPromise: (promise: Promise<UploadResponse>) => setUpload({ promise }),
|
||||
onProgress: handleProgress,
|
||||
onSuccess: (mxc) => setUpload({ mxc }),
|
||||
onError: (error) => setUpload({ error }),
|
||||
}),
|
||||
[mx, file, hideFilename, setUpload, handleProgress]
|
||||
);
|
||||
|
||||
const cancelUpload = useCallback(async () => {
|
||||
if (upload.status === UploadStatus.Loading) {
|
||||
await mx.cancelUpload(upload.promise);
|
||||
}
|
||||
}, [mx, upload]);
|
||||
|
||||
return {
|
||||
upload,
|
||||
startUpload,
|
||||
cancelUpload,
|
||||
};
|
||||
};
|
||||
|
||||
export const createUploadAtomFamily = () =>
|
||||
atomFamily<TUploadContent, TUploadAtom>(createUploadAtom);
|
||||
export type TUploadAtomFamily = ReturnType<typeof createUploadAtomFamily>;
|
||||
|
||||
export const createUploadFamilyObserverAtom = (
|
||||
uploadFamily: TUploadAtomFamily,
|
||||
uploads: TUploadContent[]
|
||||
) => atom<Upload[]>((get) => uploads.map((upload) => get(uploadFamily(upload))));
|
||||
export type TUploadFamilyObserverAtom = ReturnType<typeof createUploadFamilyObserverAtom>;
|
||||
64
src/app/state/utils.ts
Normal file
64
src/app/state/utils.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { useSetAtom, WritableAtom } from 'jotai';
|
||||
import { ClientEvent, MatrixClient, Room, RoomEvent } from 'matrix-js-sdk';
|
||||
import { useEffect } from 'react';
|
||||
import { Membership } from '../../types/matrix/room';
|
||||
|
||||
export type RoomsAction =
|
||||
| {
|
||||
type: 'INITIALIZE';
|
||||
rooms: string[];
|
||||
}
|
||||
| {
|
||||
type: 'PUT' | 'DELETE';
|
||||
roomId: string;
|
||||
};
|
||||
|
||||
export const useBindRoomsWithMembershipsAtom = (
|
||||
mx: MatrixClient,
|
||||
roomsAtom: WritableAtom<string[], RoomsAction>,
|
||||
memberships: Membership[]
|
||||
) => {
|
||||
const setRoomsAtom = useSetAtom(roomsAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const satisfyMembership = (room: Room): boolean =>
|
||||
!!memberships.find((membership) => membership === room.getMyMembership());
|
||||
setRoomsAtom({
|
||||
type: 'INITIALIZE',
|
||||
rooms: mx
|
||||
.getRooms()
|
||||
.filter(satisfyMembership)
|
||||
.map((room) => room.roomId),
|
||||
});
|
||||
|
||||
const handleAddRoom = (room: Room) => {
|
||||
if (satisfyMembership(room)) {
|
||||
setRoomsAtom({ type: 'PUT', roomId: room.roomId });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMembershipChange = (room: Room) => {
|
||||
if (!satisfyMembership(room)) {
|
||||
setRoomsAtom({ type: 'DELETE', roomId: room.roomId });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRoom = (roomId: string) => {
|
||||
setRoomsAtom({ type: 'DELETE', roomId });
|
||||
};
|
||||
|
||||
mx.on(ClientEvent.Room, handleAddRoom);
|
||||
mx.on(RoomEvent.MyMembership, handleMembershipChange);
|
||||
mx.on(ClientEvent.DeleteRoom, handleDeleteRoom);
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.Room, handleAddRoom);
|
||||
mx.removeListener(RoomEvent.MyMembership, handleMembershipChange);
|
||||
mx.removeListener(ClientEvent.DeleteRoom, handleDeleteRoom);
|
||||
};
|
||||
}, [mx, memberships, setRoomsAtom]);
|
||||
};
|
||||
|
||||
export const compareRoomsEqual = (a: string[], b: string[]) => {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((roomId, roomIdIndex) => roomId === b[roomIdIndex]);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue