mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-10 01:00:28 +03:00
redesigned app settings and switch to rust crypto (#1988)
* rework general settings * account settings - WIP * add missing key prop * add object url hook * extract wide modal styles * profile settings and image editor - WIP * add outline style to upload card * remove file param from bind upload atom hook * add compact variant to upload card * add compact upload card renderer * add option to update profile avatar * add option to change profile displayname * allow displayname change based on capabilities check * rearrange settings components into folders * add system notification settings * add initial page param in settings * convert account data hook to typescript * add push rule hook * add notification mode hook * add notification mode switcher component * add all messages notification settings options * add special messages notification settings * add keyword notifications * add ignored users section * improve ignore user list strings * add about settings * add access token option in about settings * add developer tools settings * add expand button to account data dev tool option * update folds * fix editable active element textarea check * do not close dialog when editable element in focus * add text area plugins * add text area intent handler hook * add newline intent mod in text area * add next line hotkey in text area intent hook * add syntax error position dom utility function * add account data editor * add button to send new account data in dev tools * improve custom emoji plugin * add more custom emojis hooks * add text util css * add word break in setting tile title and description * emojis and sticker user settings - WIP * view image packs from settings * emoji pack editing - WIP * add option to edit pack meta * change saved changes message * add image edit and delete controls * add option to upload pack images and apply changes * fix state event type when updating image pack * lazy load pack image tile img * hide upload image button when user can not edit pack * add option to add or remove global image packs * upgrade to rust crypto (#2168) * update matrix js sdk * remove dead code * use rust crypto * update setPowerLevel usage * fix types * fix deprecated isRoomEncrypted method uses * fix deprecated room.currentState uses * fix deprecated import/export room keys func * fix merge issues in image pack file * fix remaining issues in image pack file * start indexedDBStore * update package lock and vite-plugin-top-level-await * user session settings - WIP * add useAsync hook * add password stage uia * add uia flow matrix error hook * add UIA action component * add options to delete sessions * add sso uia stage * fix SSO stage complete error * encryption - WIP * update user settings encryption terminology * add default variant to password input * use password input in uia password stage * add options for local backup in user settings * remove typo in import local backup password input label * online backup - WIP * fix uia sso action * move access token settings from about to developer tools * merge encryption tab into sessions and rename it to devices * add device placeholder tile * add logout dialog * add logout button for current device * move other devices in component * render unverified device verification tile * add learn more section for current device verification * add device verification status badge * add info card component * add index file for password input component * add types for secret storage * add component to access secret storage key * manual verification - WIP * update matrix-js-sdk to v35 * add manual verification * use react query for device list * show unverified tab on sidebar * fix device list updates * add session key details to current device * render restore encryption backup * fix loading state of restore backup * fix unverified tab settings closes after verification * key backup tile - WIP * fix unverified tab badge * rename session key to device key in device tile * improve backup restore functionality * fix restore button enabled after layout reload during restoring backup * update backup info on status change * add backup disconnection failures * add device verification using sas * restore backup after verification * show option to logout on startup error screen * fix key backup hook update on decryption key cached * add option to enable device verification * add device verification reset dialog * add logout button in settings drawer * add encrypted message lost on logout * fix backup restore never finish with 0 keys * fix setup dialog hides when enabling device verification * show backup details in menu * update setup device verification body copy * replace deprecated method * fix displayname appear as mxid in settings * remove old refactored codes * fix types
This commit is contained in:
parent
f5d68fcc22
commit
56b754153a
196 changed files with 14171 additions and 8403 deletions
|
|
@ -1,21 +0,0 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export function useAccountData(eventType) {
|
||||
const mx = useMatrixClient();
|
||||
const [event, setEvent] = useState(mx.getAccountData(eventType));
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = (mEvent) => {
|
||||
if (mEvent.getType() !== eventType) return;
|
||||
setEvent(mEvent);
|
||||
};
|
||||
mx.on('accountData', handleChange);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleChange);
|
||||
};
|
||||
}, [mx, eventType]);
|
||||
|
||||
return event;
|
||||
}
|
||||
22
src/app/hooks/useAccountData.ts
Normal file
22
src/app/hooks/useAccountData.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useAccountDataCallback } from './useAccountDataCallback';
|
||||
|
||||
export function useAccountData(eventType: string) {
|
||||
const mx = useMatrixClient();
|
||||
const [event, setEvent] = useState(() => mx.getAccountData(eventType));
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(evt) => {
|
||||
if (evt.getType() === eventType) {
|
||||
setEvent(evt);
|
||||
}
|
||||
},
|
||||
[eventType, setEvent]
|
||||
)
|
||||
);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useAlive } from './useAlive';
|
||||
|
||||
|
|
@ -31,12 +31,10 @@ export type AsyncState<D, E = unknown> = AsyncIdle | AsyncLoading | AsyncSuccess
|
|||
|
||||
export type AsyncCallback<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData>;
|
||||
|
||||
export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
||||
asyncCallback: AsyncCallback<TArgs, TData>
|
||||
): [AsyncState<TData, TError>, AsyncCallback<TArgs, TData>] => {
|
||||
const [state, setState] = useState<AsyncState<TData, TError>>({
|
||||
status: AsyncStatus.Idle,
|
||||
});
|
||||
export const useAsync = <TData, TError, TArgs extends unknown[]>(
|
||||
asyncCallback: AsyncCallback<TArgs, TData>,
|
||||
onStateChange: (state: AsyncState<TData, TError>) => void
|
||||
): AsyncCallback<TArgs, TData> => {
|
||||
const alive = useAlive();
|
||||
|
||||
// Tracks the request number.
|
||||
|
|
@ -53,7 +51,7 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
|||
flushSync(() => {
|
||||
// flushSync because
|
||||
// https://github.com/facebook/react/issues/26713#issuecomment-1872085134
|
||||
setState({
|
||||
onStateChange({
|
||||
status: AsyncStatus.Loading,
|
||||
});
|
||||
});
|
||||
|
|
@ -69,7 +67,7 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
|||
}
|
||||
if (alive()) {
|
||||
queueMicrotask(() => {
|
||||
setState({
|
||||
onStateChange({
|
||||
status: AsyncStatus.Success,
|
||||
data,
|
||||
});
|
||||
|
|
@ -83,7 +81,7 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
|||
|
||||
if (alive()) {
|
||||
queueMicrotask(() => {
|
||||
setState({
|
||||
onStateChange({
|
||||
status: AsyncStatus.Error,
|
||||
error: e as TError,
|
||||
});
|
||||
|
|
@ -92,8 +90,32 @@ export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
|||
throw e;
|
||||
}
|
||||
},
|
||||
[asyncCallback, alive]
|
||||
[asyncCallback, alive, onStateChange]
|
||||
);
|
||||
|
||||
return callback;
|
||||
};
|
||||
|
||||
export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
|
||||
asyncCallback: AsyncCallback<TArgs, TData>
|
||||
): [AsyncState<TData, TError>, AsyncCallback<TArgs, TData>] => {
|
||||
const [state, setState] = useState<AsyncState<TData, TError>>({
|
||||
status: AsyncStatus.Idle,
|
||||
});
|
||||
|
||||
const callback = useAsync(asyncCallback, setState);
|
||||
|
||||
return [state, callback];
|
||||
};
|
||||
|
||||
export const useAsyncCallbackValue = <TData, TError>(
|
||||
asyncCallback: AsyncCallback<[], TData>
|
||||
): [AsyncState<TData, TError>, AsyncCallback<[], TData>] => {
|
||||
const [state, load] = useAsyncCallback<TData, TError, []>(asyncCallback);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
return [state, load];
|
||||
};
|
||||
|
|
|
|||
9
src/app/hooks/useCrossSigning.ts
Normal file
9
src/app/hooks/useCrossSigning.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { AccountDataEvent, SecretAccountData } from '../../types/matrix/accountData';
|
||||
import { useAccountData } from './useAccountData';
|
||||
|
||||
export const useCrossSigningActive = (): boolean => {
|
||||
const masterEvent = useAccountData(AccountDataEvent.CrossSigningMaster);
|
||||
const content = masterEvent?.getContent<SecretAccountData>();
|
||||
|
||||
return !!content;
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export function useCrossSigningStatus() {
|
||||
const mx = useMatrixClient();
|
||||
const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData(mx));
|
||||
|
||||
useEffect(() => {
|
||||
if (isCSEnabled) return undefined;
|
||||
const handleAccountData = (event) => {
|
||||
if (event.getType() === 'm.cross_signing.master') {
|
||||
setIsCSEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
mx.on('accountData', handleAccountData);
|
||||
return () => {
|
||||
mx.removeListener('accountData', handleAccountData);
|
||||
};
|
||||
}, [mx, isCSEnabled]);
|
||||
return isCSEnabled;
|
||||
}
|
||||
|
|
@ -1,35 +1,77 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CryptoEvent, IMyDevice } from 'matrix-js-sdk';
|
||||
import { CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { IMyDevice } from 'matrix-js-sdk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export function useDeviceList() {
|
||||
export const useDeviceListChange = (
|
||||
onChange: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated]
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
const [deviceList, setDeviceList] = useState<IMyDevice[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const updateDevices = () =>
|
||||
mx.getDevices().then((data) => {
|
||||
if (!isMounted) return;
|
||||
setDeviceList(data.devices || []);
|
||||
});
|
||||
updateDevices();
|
||||
|
||||
const handleDevicesUpdate: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] = (users) => {
|
||||
const userId = mx.getUserId();
|
||||
if (userId && users.includes(userId)) {
|
||||
updateDevices();
|
||||
}
|
||||
};
|
||||
|
||||
mx.on(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||
mx.on(CryptoEvent.DevicesUpdated, onChange);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.DevicesUpdated, handleDevicesUpdate);
|
||||
isMounted = false;
|
||||
mx.removeListener(CryptoEvent.DevicesUpdated, onChange);
|
||||
};
|
||||
}, [mx, onChange]);
|
||||
};
|
||||
|
||||
const DEVICES_QUERY_KEY = ['devices'];
|
||||
|
||||
export function useDeviceList(): [undefined | IMyDevice[], () => Promise<void>] {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const fetchDevices = useCallback(async () => {
|
||||
const data = await mx.getDevices();
|
||||
return data.devices ?? [];
|
||||
}, [mx]);
|
||||
return deviceList;
|
||||
|
||||
const { data: deviceList, refetch } = useQuery({
|
||||
queryKey: DEVICES_QUERY_KEY,
|
||||
queryFn: fetchDevices,
|
||||
staleTime: 0,
|
||||
gcTime: Infinity,
|
||||
refetchOnMount: 'always',
|
||||
});
|
||||
|
||||
const refreshDeviceList = useCallback(async () => {
|
||||
await refetch();
|
||||
}, [refetch]);
|
||||
|
||||
useDeviceListChange(
|
||||
useCallback(
|
||||
(users) => {
|
||||
const userId = mx.getUserId();
|
||||
if (userId && users.includes(userId)) {
|
||||
refreshDeviceList();
|
||||
}
|
||||
},
|
||||
[mx, refreshDeviceList]
|
||||
)
|
||||
);
|
||||
|
||||
return [deviceList ?? undefined, refreshDeviceList];
|
||||
}
|
||||
|
||||
export const useDeviceIds = (devices: IMyDevice[] | undefined): string[] => {
|
||||
const devicesId = useMemo(() => devices?.map((device) => device.device_id) ?? [], [devices]);
|
||||
|
||||
return devicesId;
|
||||
};
|
||||
|
||||
export const useSplitCurrentDevice = (
|
||||
devices: IMyDevice[] | undefined
|
||||
): [IMyDevice | undefined, IMyDevice[] | undefined] => {
|
||||
const mx = useMatrixClient();
|
||||
const currentDeviceId = mx.getDeviceId();
|
||||
const currentDevice = useMemo(
|
||||
() => devices?.find((d) => d.device_id === currentDeviceId),
|
||||
[devices, currentDeviceId]
|
||||
);
|
||||
const otherDevices = useMemo(
|
||||
() => devices?.filter((device) => device.device_id !== currentDeviceId),
|
||||
[devices, currentDeviceId]
|
||||
);
|
||||
|
||||
return [currentDevice, otherDevices];
|
||||
};
|
||||
|
|
|
|||
106
src/app/hooks/useDeviceVerificationStatus.ts
Normal file
106
src/app/hooks/useDeviceVerificationStatus.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { CryptoApi } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { verifiedDevice } from '../utils/matrix-crypto';
|
||||
import { useAlive } from './useAlive';
|
||||
import { fulfilledPromiseSettledResult } from '../utils/common';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useDeviceListChange } from './useDeviceList';
|
||||
|
||||
export enum VerificationStatus {
|
||||
Unknown,
|
||||
Unverified,
|
||||
Verified,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
export const useDeviceVerificationDetect = (
|
||||
crypto: CryptoApi | undefined,
|
||||
userId: string,
|
||||
deviceId: string | undefined,
|
||||
callback: (status: VerificationStatus) => void
|
||||
): void => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const updateStatus = useCallback(async () => {
|
||||
if (crypto && deviceId) {
|
||||
const data = await verifiedDevice(crypto, userId, deviceId);
|
||||
if (data === null) {
|
||||
callback(VerificationStatus.Unsupported);
|
||||
return;
|
||||
}
|
||||
callback(data ? VerificationStatus.Verified : VerificationStatus.Unverified);
|
||||
return;
|
||||
}
|
||||
callback(VerificationStatus.Unknown);
|
||||
}, [crypto, deviceId, userId, callback]);
|
||||
|
||||
useEffect(() => {
|
||||
updateStatus();
|
||||
}, [mx, updateStatus, userId]);
|
||||
|
||||
useDeviceListChange(
|
||||
useCallback(
|
||||
(userIds) => {
|
||||
if (userIds.includes(userId)) {
|
||||
updateStatus();
|
||||
}
|
||||
},
|
||||
[userId, updateStatus]
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const useDeviceVerificationStatus = (
|
||||
crypto: CryptoApi | undefined,
|
||||
userId: string,
|
||||
deviceId: string | undefined
|
||||
): VerificationStatus => {
|
||||
const [verificationStatus, setVerificationStatus] = useState(VerificationStatus.Unknown);
|
||||
|
||||
useDeviceVerificationDetect(crypto, userId, deviceId, setVerificationStatus);
|
||||
|
||||
return verificationStatus;
|
||||
};
|
||||
|
||||
export const useUnverifiedDeviceCount = (
|
||||
crypto: CryptoApi | undefined,
|
||||
userId: string,
|
||||
devices: string[]
|
||||
): number | undefined => {
|
||||
const [unverifiedCount, setUnverifiedCount] = useState<number>();
|
||||
const alive = useAlive();
|
||||
|
||||
const updateCount = useCallback(async () => {
|
||||
let count = 0;
|
||||
if (crypto) {
|
||||
const promises = devices.map((deviceId) => verifiedDevice(crypto, userId, deviceId));
|
||||
const result = await Promise.allSettled(promises);
|
||||
const settledResult = fulfilledPromiseSettledResult(result);
|
||||
settledResult.forEach((status) => {
|
||||
if (status === false) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (alive()) {
|
||||
setUnverifiedCount(count);
|
||||
}
|
||||
}, [crypto, userId, devices, alive]);
|
||||
|
||||
useDeviceListChange(
|
||||
useCallback(
|
||||
(userIds) => {
|
||||
if (userIds.includes(userId)) {
|
||||
updateCount();
|
||||
}
|
||||
},
|
||||
[userId, updateCount]
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateCount();
|
||||
}, [updateCount]);
|
||||
|
||||
return unverifiedCount;
|
||||
};
|
||||
|
|
@ -1,48 +1,161 @@
|
|||
import { ClientEvent, MatrixClient, MatrixEvent, Room, RoomStateEvent } from 'matrix-js-sdk';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { getRelevantPacks, ImagePack, PackUsage } from '../plugins/custom-emoji';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { useForceUpdate } from './useForceUpdate';
|
||||
import {
|
||||
getGlobalImagePacks,
|
||||
getRoomImagePack,
|
||||
getRoomImagePacks,
|
||||
getUserImagePack,
|
||||
ImagePack,
|
||||
ImageUsage,
|
||||
} from '../plugins/custom-emoji';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useAccountDataCallback } from './useAccountDataCallback';
|
||||
import { useStateEventCallback } from './useStateEventCallback';
|
||||
|
||||
export const useRelevantImagePacks = (
|
||||
mx: MatrixClient,
|
||||
usage: PackUsage,
|
||||
rooms: Room[]
|
||||
): ImagePack[] => {
|
||||
const [forceCount, forceUpdate] = useForceUpdate();
|
||||
export const useUserImagePack = (): ImagePack | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
const [userPack, setUserPack] = useState(() => getUserImagePack(mx));
|
||||
|
||||
const relevantPacks = useMemo(
|
||||
() => getRelevantPacks(mx, rooms).filter((pack) => pack.getImagesFor(usage).length > 0),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[mx, usage, rooms, forceCount]
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (mEvent.getType() === AccountDataEvent.PoniesUserEmotes) {
|
||||
setUserPack(getUserImagePack(mx));
|
||||
}
|
||||
},
|
||||
[mx]
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdate = (event: MatrixEvent) => {
|
||||
if (
|
||||
event.getType() === AccountDataEvent.PoniesEmoteRooms ||
|
||||
event.getType() === AccountDataEvent.PoniesUserEmotes
|
||||
) {
|
||||
forceUpdate();
|
||||
}
|
||||
const eventRoomId = event.getRoomId();
|
||||
if (
|
||||
eventRoomId &&
|
||||
event.getType() === StateEvent.PoniesRoomEmotes &&
|
||||
rooms.find((room) => room.roomId === eventRoomId)
|
||||
) {
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
return userPack;
|
||||
};
|
||||
|
||||
mx.on(ClientEvent.AccountData, handleUpdate);
|
||||
mx.on(RoomStateEvent.Events, handleUpdate);
|
||||
return () => {
|
||||
mx.removeListener(ClientEvent.AccountData, handleUpdate);
|
||||
mx.removeListener(RoomStateEvent.Events, handleUpdate);
|
||||
};
|
||||
}, [mx, rooms, forceUpdate]);
|
||||
export const useGlobalImagePacks = (): ImagePack[] => {
|
||||
const mx = useMatrixClient();
|
||||
const [globalPacks, setGlobalPacks] = useState(() => getGlobalImagePacks(mx));
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (mEvent.getType() === AccountDataEvent.PoniesEmoteRooms) {
|
||||
setGlobalPacks(getGlobalImagePacks(mx));
|
||||
}
|
||||
},
|
||||
[mx]
|
||||
)
|
||||
);
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
const eventType = mEvent.getType();
|
||||
const roomId = mEvent.getRoomId();
|
||||
const stateKey = mEvent.getStateKey();
|
||||
if (eventType === StateEvent.PoniesRoomEmotes && roomId && typeof stateKey === 'string') {
|
||||
const global = !!globalPacks.find(
|
||||
(pack) =>
|
||||
pack.address && pack.address.roomId === roomId && pack.address.stateKey === stateKey
|
||||
);
|
||||
if (global) {
|
||||
setGlobalPacks(getGlobalImagePacks(mx));
|
||||
}
|
||||
}
|
||||
},
|
||||
[mx, globalPacks]
|
||||
)
|
||||
);
|
||||
|
||||
return globalPacks;
|
||||
};
|
||||
|
||||
export const useRoomImagePack = (room: Room, stateKey: string): ImagePack | undefined => {
|
||||
const mx = useMatrixClient();
|
||||
const [roomPack, setRoomPack] = useState(() => getRoomImagePack(room, stateKey));
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (
|
||||
mEvent.getRoomId() === room.roomId &&
|
||||
mEvent.getType() === StateEvent.PoniesRoomEmotes &&
|
||||
mEvent.getStateKey() === stateKey
|
||||
) {
|
||||
setRoomPack(getRoomImagePack(room, stateKey));
|
||||
}
|
||||
},
|
||||
[room, stateKey]
|
||||
)
|
||||
);
|
||||
|
||||
return roomPack;
|
||||
};
|
||||
|
||||
export const useRoomImagePacks = (room: Room): ImagePack[] => {
|
||||
const mx = useMatrixClient();
|
||||
const [roomPacks, setRoomPacks] = useState(() => getRoomImagePacks(room));
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (
|
||||
mEvent.getRoomId() === room.roomId &&
|
||||
mEvent.getType() === StateEvent.PoniesRoomEmotes
|
||||
) {
|
||||
setRoomPacks(getRoomImagePacks(room));
|
||||
}
|
||||
},
|
||||
[room]
|
||||
)
|
||||
);
|
||||
|
||||
return roomPacks;
|
||||
};
|
||||
|
||||
export const useRoomsImagePacks = (rooms: Room[]) => {
|
||||
const mx = useMatrixClient();
|
||||
const [roomPacks, setRoomPacks] = useState(() => rooms.flatMap(getRoomImagePacks));
|
||||
|
||||
useStateEventCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(mEvent) => {
|
||||
if (
|
||||
rooms.find((room) => room.roomId === mEvent.getRoomId()) &&
|
||||
mEvent.getType() === StateEvent.PoniesRoomEmotes
|
||||
) {
|
||||
setRoomPacks(rooms.flatMap(getRoomImagePacks));
|
||||
}
|
||||
},
|
||||
[rooms]
|
||||
)
|
||||
);
|
||||
|
||||
return roomPacks;
|
||||
};
|
||||
|
||||
export const useRelevantImagePacks = (usage: ImageUsage, rooms: Room[]): ImagePack[] => {
|
||||
const userPack = useUserImagePack();
|
||||
const globalPacks = useGlobalImagePacks();
|
||||
const roomsPacks = useRoomsImagePacks(rooms);
|
||||
|
||||
const relevantPacks = useMemo(() => {
|
||||
const packs = userPack ? [userPack] : [];
|
||||
const globalPackIds = new Set(globalPacks.map((pack) => pack.id));
|
||||
|
||||
const relPacks = packs.concat(
|
||||
globalPacks,
|
||||
roomsPacks.filter((pack) => !globalPackIds.has(pack.id))
|
||||
);
|
||||
|
||||
return relPacks.filter((pack) => pack.getImages(usage).length > 0);
|
||||
}, [userPack, globalPacks, roomsPacks, usage]);
|
||||
|
||||
return relevantPacks;
|
||||
};
|
||||
|
|
|
|||
160
src/app/hooks/useKeyBackup.ts
Normal file
160
src/app/hooks/useKeyBackup.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import {
|
||||
BackupTrustInfo,
|
||||
CryptoApi,
|
||||
CryptoEvent,
|
||||
CryptoEventHandlerMap,
|
||||
KeyBackupInfo,
|
||||
} from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useAlive } from './useAlive';
|
||||
|
||||
export const useKeyBackupStatusChange = (
|
||||
onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupStatus]
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
mx.on(CryptoEvent.KeyBackupStatus, onChange);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.KeyBackupStatus, onChange);
|
||||
};
|
||||
}, [mx, onChange]);
|
||||
};
|
||||
|
||||
export const useKeyBackupStatus = (crypto: CryptoApi): boolean => {
|
||||
const alive = useAlive();
|
||||
const [status, setStatus] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
crypto.getActiveSessionBackupVersion().then((v) => {
|
||||
if (alive()) {
|
||||
setStatus(typeof v === 'string');
|
||||
}
|
||||
});
|
||||
}, [crypto, alive]);
|
||||
|
||||
useKeyBackupStatusChange(setStatus);
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
export const useKeyBackupSessionsRemainingChange = (
|
||||
onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupSessionsRemaining]
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
mx.on(CryptoEvent.KeyBackupSessionsRemaining, onChange);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.KeyBackupSessionsRemaining, onChange);
|
||||
};
|
||||
}, [mx, onChange]);
|
||||
};
|
||||
|
||||
export const useKeyBackupFailedChange = (
|
||||
onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupFailed]
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
mx.on(CryptoEvent.KeyBackupFailed, onChange);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.KeyBackupFailed, onChange);
|
||||
};
|
||||
}, [mx, onChange]);
|
||||
};
|
||||
|
||||
export const useKeyBackupDecryptionKeyCached = (
|
||||
onChange: CryptoEventHandlerMap[CryptoEvent.KeyBackupDecryptionKeyCached]
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
mx.on(CryptoEvent.KeyBackupDecryptionKeyCached, onChange);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.KeyBackupDecryptionKeyCached, onChange);
|
||||
};
|
||||
}, [mx, onChange]);
|
||||
};
|
||||
|
||||
export const useKeyBackupSync = (): [number, string | undefined] => {
|
||||
const [remaining, setRemaining] = useState(0);
|
||||
const [failure, setFailure] = useState<string>();
|
||||
|
||||
useKeyBackupSessionsRemainingChange(
|
||||
useCallback((count) => {
|
||||
setRemaining(count);
|
||||
setFailure(undefined);
|
||||
}, [])
|
||||
);
|
||||
|
||||
useKeyBackupFailedChange(
|
||||
useCallback((f) => {
|
||||
if (typeof f === 'string') {
|
||||
setFailure(f);
|
||||
setRemaining(0);
|
||||
}
|
||||
}, [])
|
||||
);
|
||||
|
||||
return [remaining, failure];
|
||||
};
|
||||
|
||||
export const useKeyBackupInfo = (crypto: CryptoApi): KeyBackupInfo | undefined | null => {
|
||||
const alive = useAlive();
|
||||
const [info, setInfo] = useState<KeyBackupInfo | null>();
|
||||
|
||||
const fetchInfo = useCallback(() => {
|
||||
crypto.getKeyBackupInfo().then((i) => {
|
||||
if (alive()) {
|
||||
setInfo(i);
|
||||
}
|
||||
});
|
||||
}, [crypto, alive]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInfo();
|
||||
}, [fetchInfo]);
|
||||
|
||||
useKeyBackupStatusChange(fetchInfo);
|
||||
|
||||
useKeyBackupSessionsRemainingChange(
|
||||
useCallback(
|
||||
(remainingCount) => {
|
||||
if (remainingCount === 0) {
|
||||
fetchInfo();
|
||||
}
|
||||
},
|
||||
[fetchInfo]
|
||||
)
|
||||
);
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
export const useKeyBackupTrust = (
|
||||
crypto: CryptoApi,
|
||||
backupInfo: KeyBackupInfo
|
||||
): BackupTrustInfo | undefined => {
|
||||
const alive = useAlive();
|
||||
const [trust, setTrust] = useState<BackupTrustInfo>();
|
||||
|
||||
const fetchTrust = useCallback(() => {
|
||||
crypto.isKeyBackupTrusted(backupInfo).then((t) => {
|
||||
if (alive()) {
|
||||
setTrust(t);
|
||||
}
|
||||
});
|
||||
}, [crypto, alive, backupInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTrust();
|
||||
}, [fetchTrust]);
|
||||
|
||||
useKeyBackupStatusChange(fetchTrust);
|
||||
|
||||
useKeyBackupDecryptionKeyCached(fetchTrust);
|
||||
|
||||
return trust;
|
||||
};
|
||||
26
src/app/hooks/useMessageLayout.ts
Normal file
26
src/app/hooks/useMessageLayout.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useMemo } from 'react';
|
||||
import { MessageLayout } from '../state/settings';
|
||||
|
||||
export type MessageLayoutItem = {
|
||||
name: string;
|
||||
layout: MessageLayout;
|
||||
};
|
||||
|
||||
export const useMessageLayoutItems = (): MessageLayoutItem[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
layout: MessageLayout.Modern,
|
||||
name: 'Modern',
|
||||
},
|
||||
{
|
||||
layout: MessageLayout.Compact,
|
||||
name: 'Compact',
|
||||
},
|
||||
{
|
||||
layout: MessageLayout.Bubble,
|
||||
name: 'Bubble',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
38
src/app/hooks/useMessageSpacing.ts
Normal file
38
src/app/hooks/useMessageSpacing.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useMemo } from 'react';
|
||||
import { MessageSpacing } from '../state/settings';
|
||||
|
||||
export type MessageSpacingItem = {
|
||||
name: string;
|
||||
spacing: MessageSpacing;
|
||||
};
|
||||
|
||||
export const useMessageSpacingItems = (): MessageSpacingItem[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
{
|
||||
spacing: '0',
|
||||
name: 'None',
|
||||
},
|
||||
{
|
||||
spacing: '100',
|
||||
name: 'Ultra Small',
|
||||
},
|
||||
{
|
||||
spacing: '200',
|
||||
name: 'Extra Small',
|
||||
},
|
||||
{
|
||||
spacing: '300',
|
||||
name: 'Small',
|
||||
},
|
||||
{
|
||||
spacing: '400',
|
||||
name: 'Normal',
|
||||
},
|
||||
{
|
||||
spacing: '500',
|
||||
name: 'Large',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
66
src/app/hooks/useNotificationMode.ts
Normal file
66
src/app/hooks/useNotificationMode.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { PushRuleAction, PushRuleActionName, TweakName } from 'matrix-js-sdk';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export enum NotificationMode {
|
||||
OFF = 'OFF',
|
||||
Notify = 'Notify',
|
||||
NotifyLoud = 'NotifyLoud',
|
||||
}
|
||||
|
||||
export type NotificationModeOptions = {
|
||||
soundValue?: string;
|
||||
highlight?: boolean;
|
||||
};
|
||||
export const getNotificationModeActions = (
|
||||
mode: NotificationMode,
|
||||
options?: NotificationModeOptions
|
||||
): PushRuleAction[] => {
|
||||
if (mode === NotificationMode.OFF) return [];
|
||||
|
||||
const actions: PushRuleAction[] = [PushRuleActionName.Notify];
|
||||
|
||||
if (mode === NotificationMode.NotifyLoud) {
|
||||
actions.push({
|
||||
set_tweak: TweakName.Sound,
|
||||
value: options?.soundValue ?? 'default',
|
||||
});
|
||||
}
|
||||
|
||||
if (options?.highlight) {
|
||||
actions.push({
|
||||
set_tweak: TweakName.Highlight,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
export type GetNotificationModeCallback = (mode: NotificationMode) => PushRuleAction[];
|
||||
export const useNotificationModeActions = (
|
||||
options?: NotificationModeOptions
|
||||
): GetNotificationModeCallback => {
|
||||
const getAction: GetNotificationModeCallback = useCallback(
|
||||
(mode) => getNotificationModeActions(mode, options),
|
||||
[options]
|
||||
);
|
||||
|
||||
return getAction;
|
||||
};
|
||||
|
||||
export const useNotificationActionsMode = (actions: PushRuleAction[]): NotificationMode => {
|
||||
const mode: NotificationMode = useMemo(() => {
|
||||
const soundTweak = actions.find(
|
||||
(action) => typeof action === 'object' && action.set_tweak === TweakName.Sound
|
||||
);
|
||||
const notify = actions.find(
|
||||
(action) => typeof action === 'string' && action === PushRuleActionName.Notify
|
||||
);
|
||||
|
||||
if (notify && soundTweak) return NotificationMode.NotifyLoud;
|
||||
if (notify) return NotificationMode.Notify;
|
||||
return NotificationMode.OFF;
|
||||
}, [actions]);
|
||||
|
||||
return mode;
|
||||
};
|
||||
17
src/app/hooks/useObjectURL.ts
Normal file
17
src/app/hooks/useObjectURL.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
export const useObjectURL = (object?: Blob): string | undefined => {
|
||||
const url = useMemo(() => {
|
||||
if (object) return URL.createObjectURL(object);
|
||||
return undefined;
|
||||
}, [object]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (url) URL.revokeObjectURL(url);
|
||||
},
|
||||
[url]
|
||||
);
|
||||
|
||||
return url;
|
||||
};
|
||||
71
src/app/hooks/usePushRule.ts
Normal file
71
src/app/hooks/usePushRule.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import {
|
||||
IPushRule,
|
||||
IPushRules,
|
||||
PushRuleAction,
|
||||
PushRuleCondition,
|
||||
PushRuleKind,
|
||||
RuleId,
|
||||
} from 'matrix-js-sdk';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export type PushRuleData = {
|
||||
kind: PushRuleKind;
|
||||
pushRule: IPushRule;
|
||||
};
|
||||
|
||||
export const makePushRuleData = (
|
||||
kind: PushRuleKind,
|
||||
ruleId: RuleId,
|
||||
actions: PushRuleAction[],
|
||||
conditions?: PushRuleCondition[],
|
||||
pattern?: string,
|
||||
enabled?: boolean,
|
||||
_default?: boolean
|
||||
): PushRuleData => ({
|
||||
kind,
|
||||
pushRule: {
|
||||
rule_id: ruleId,
|
||||
default: _default ?? true,
|
||||
enabled: enabled ?? true,
|
||||
pattern,
|
||||
conditions,
|
||||
actions,
|
||||
},
|
||||
});
|
||||
|
||||
export const orderedPushRuleKinds: PushRuleKind[] = [
|
||||
PushRuleKind.Override,
|
||||
PushRuleKind.ContentSpecific,
|
||||
PushRuleKind.RoomSpecific,
|
||||
PushRuleKind.SenderSpecific,
|
||||
PushRuleKind.Underride,
|
||||
];
|
||||
|
||||
export const getPushRule = (
|
||||
pushRules: IPushRules,
|
||||
ruleId: RuleId | string
|
||||
): PushRuleData | undefined => {
|
||||
const { global } = pushRules;
|
||||
|
||||
let ruleData: PushRuleData | undefined;
|
||||
|
||||
orderedPushRuleKinds.some((kind) => {
|
||||
const rules = global[kind];
|
||||
const pushRule = rules?.find((r) => r.rule_id === ruleId);
|
||||
if (pushRule) {
|
||||
ruleData = {
|
||||
kind,
|
||||
pushRule,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return ruleData;
|
||||
};
|
||||
|
||||
export const usePushRule = (
|
||||
pushRules: IPushRules,
|
||||
ruleId: RuleId | string
|
||||
): PushRuleData | undefined => useMemo(() => getPushRule(pushRules, ruleId), [pushRules, ruleId]);
|
||||
24
src/app/hooks/useRestoreBackupOnVerification.ts
Normal file
24
src/app/hooks/useRestoreBackupOnVerification.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { backupRestoreProgressAtom } from '../state/backupRestore';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useKeyBackupDecryptionKeyCached } from './useKeyBackup';
|
||||
|
||||
export const useRestoreBackupOnVerification = () => {
|
||||
const setRestoreProgress = useSetAtom(backupRestoreProgressAtom);
|
||||
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useKeyBackupDecryptionKeyCached(
|
||||
useCallback(() => {
|
||||
const crypto = mx.getCrypto();
|
||||
if (!crypto) return;
|
||||
|
||||
crypto.restoreKeyBackup({
|
||||
progressCallback(progress) {
|
||||
setRestoreProgress(progress);
|
||||
},
|
||||
});
|
||||
}, [mx, setRestoreProgress])
|
||||
);
|
||||
};
|
||||
22
src/app/hooks/useSecretStorage.ts
Normal file
22
src/app/hooks/useSecretStorage.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
AccountDataEvent,
|
||||
SecretStorageDefaultKeyContent,
|
||||
SecretStorageKeyContent,
|
||||
} from '../../types/matrix/accountData';
|
||||
import { useAccountData } from './useAccountData';
|
||||
|
||||
export const getSecretStorageKeyEventType = (key: string): string => `m.secret_storage.key.${key}`;
|
||||
|
||||
export const useSecretStorageDefaultKeyId = (): string | undefined => {
|
||||
const defaultKeyEvent = useAccountData(AccountDataEvent.SecretStorageDefaultKey);
|
||||
const defaultKeyId = defaultKeyEvent?.getContent<SecretStorageDefaultKeyContent>().key;
|
||||
|
||||
return defaultKeyId;
|
||||
};
|
||||
|
||||
export const useSecretStorageKeyContent = (keyId: string): SecretStorageKeyContent | undefined => {
|
||||
const keyEvent = useAccountData(getSecretStorageKeyEventType(keyId));
|
||||
const secretStorageKey = keyEvent?.getContent<SecretStorageKeyContent>();
|
||||
|
||||
return secretStorageKey;
|
||||
};
|
||||
58
src/app/hooks/useTextAreaIntent.ts
Normal file
58
src/app/hooks/useTextAreaIntent.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { KeyboardEventHandler, useCallback } from 'react';
|
||||
import { Cursor, Intent, Operations, TextArea } from '../plugins/text-area';
|
||||
|
||||
export const useTextAreaIntentHandler = (
|
||||
textArea: TextArea,
|
||||
operations: Operations,
|
||||
intent: Intent
|
||||
) => {
|
||||
const handler: KeyboardEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
(evt) => {
|
||||
const target = evt.currentTarget;
|
||||
|
||||
if (isKeyHotkey('tab', evt)) {
|
||||
evt.preventDefault();
|
||||
|
||||
const cursor = Cursor.fromTextAreaElement(target);
|
||||
if (textArea.selection(cursor)) {
|
||||
operations.select(intent.moveForward(cursor));
|
||||
} else {
|
||||
operations.deselect(operations.insert(cursor, intent.str));
|
||||
}
|
||||
|
||||
target.focus();
|
||||
}
|
||||
if (isKeyHotkey('shift+tab', evt)) {
|
||||
evt.preventDefault();
|
||||
const cursor = Cursor.fromTextAreaElement(target);
|
||||
const intentCursor = intent.moveBackward(cursor);
|
||||
if (textArea.selection(cursor)) {
|
||||
operations.select(intentCursor);
|
||||
} else {
|
||||
operations.deselect(intentCursor);
|
||||
}
|
||||
|
||||
target.focus();
|
||||
}
|
||||
if (isKeyHotkey('enter', evt) || isKeyHotkey('shift+enter', evt)) {
|
||||
evt.preventDefault();
|
||||
const cursor = Cursor.fromTextAreaElement(target);
|
||||
operations.select(intent.addNewLine(cursor));
|
||||
}
|
||||
if (isKeyHotkey('mod+enter', evt)) {
|
||||
evt.preventDefault();
|
||||
const cursor = Cursor.fromTextAreaElement(target);
|
||||
operations.select(intent.addNextLine(cursor));
|
||||
}
|
||||
if (isKeyHotkey('mod+shift+enter', evt)) {
|
||||
evt.preventDefault();
|
||||
const cursor = Cursor.fromTextAreaElement(target);
|
||||
operations.select(intent.addPreviousLine(cursor));
|
||||
}
|
||||
},
|
||||
[textArea, operations, intent]
|
||||
);
|
||||
|
||||
return handler;
|
||||
};
|
||||
74
src/app/hooks/useTheme.ts
Normal file
74
src/app/hooks/useTheme.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { lightTheme } from 'folds';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||
import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
|
||||
|
||||
export enum ThemeKind {
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
}
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
kind: ThemeKind;
|
||||
classNames: string[];
|
||||
};
|
||||
|
||||
export const LightTheme: Theme = {
|
||||
id: 'light-theme',
|
||||
kind: ThemeKind.Light,
|
||||
classNames: [lightTheme, onLightFontWeight, 'prism-light'],
|
||||
};
|
||||
|
||||
export const SilverTheme: Theme = {
|
||||
id: 'silver-theme',
|
||||
kind: ThemeKind.Light,
|
||||
classNames: ['silver-theme', silverTheme, onLightFontWeight, 'prism-light'],
|
||||
};
|
||||
export const DarkTheme: Theme = {
|
||||
id: 'dark-theme',
|
||||
kind: ThemeKind.Dark,
|
||||
classNames: ['dark-theme', darkTheme, onDarkFontWeight, 'prism-dark'],
|
||||
};
|
||||
export const ButterTheme: Theme = {
|
||||
id: 'butter-theme',
|
||||
kind: ThemeKind.Dark,
|
||||
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
|
||||
};
|
||||
|
||||
export const useThemes = (): Theme[] => {
|
||||
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
|
||||
|
||||
return themes;
|
||||
};
|
||||
|
||||
export const useThemeNames = (): Record<string, string> =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[LightTheme.id]: 'Light',
|
||||
[SilverTheme.id]: 'Silver',
|
||||
[DarkTheme.id]: 'Dark',
|
||||
[ButterTheme.id]: 'Butter',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
export const useSystemThemeKind = (): ThemeKind => {
|
||||
const darkModeQueryList = useMemo(() => window.matchMedia('(prefers-color-scheme: dark)'), []);
|
||||
const [themeKind, setThemeKind] = useState<ThemeKind>(
|
||||
darkModeQueryList.matches ? ThemeKind.Dark : ThemeKind.Light
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMediaQueryChange = () => {
|
||||
setThemeKind(darkModeQueryList.matches ? ThemeKind.Dark : ThemeKind.Light);
|
||||
};
|
||||
|
||||
darkModeQueryList.addEventListener('change', handleMediaQueryChange);
|
||||
return () => {
|
||||
darkModeQueryList.removeEventListener('change', handleMediaQueryChange);
|
||||
};
|
||||
}, [darkModeQueryList, setThemeKind]);
|
||||
|
||||
return themeKind;
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk';
|
||||
import { AuthType, IAuthData, MatrixError, UIAFlow } from 'matrix-js-sdk';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
getSupportedUIAFlows,
|
||||
|
|
@ -94,3 +94,12 @@ export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterf
|
|||
getStageInfo,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUIAMatrixError = (
|
||||
error?: MatrixError
|
||||
): [undefined, undefined] | [IAuthData, undefined] | [undefined, MatrixError] => {
|
||||
if (!error) return [undefined, undefined];
|
||||
if (error.httpStatus === 401) return [error.data as IAuthData, undefined];
|
||||
|
||||
return [undefined, error];
|
||||
};
|
||||
|
|
|
|||
51
src/app/hooks/useUserProfile.ts
Normal file
51
src/app/hooks/useUserProfile.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export type UserProfile = {
|
||||
avatarUrl?: string;
|
||||
displayName?: string;
|
||||
};
|
||||
export const useUserProfile = (userId: string): UserProfile => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [profile, setProfile] = useState<UserProfile>(() => {
|
||||
const user = mx.getUser(userId);
|
||||
return {
|
||||
avatarUrl: user?.avatarUrl,
|
||||
displayName: user?.displayName,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const user = mx.getUser(userId);
|
||||
const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => {
|
||||
setProfile((cp) => ({
|
||||
...cp,
|
||||
avatarUrl: myUser.avatarUrl,
|
||||
}));
|
||||
};
|
||||
const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => {
|
||||
setProfile((cp) => ({
|
||||
...cp,
|
||||
displayName: myUser.displayName,
|
||||
}));
|
||||
};
|
||||
|
||||
mx.getProfileInfo(userId).then((info) =>
|
||||
setProfile({
|
||||
avatarUrl: info.avatar_url,
|
||||
displayName: info.displayname,
|
||||
})
|
||||
);
|
||||
|
||||
user?.on(UserEvent.AvatarUrl, onAvatarChange);
|
||||
user?.on(UserEvent.DisplayName, onDisplayNameChange);
|
||||
return () => {
|
||||
user?.removeListener(UserEvent.AvatarUrl, onAvatarChange);
|
||||
user?.removeListener(UserEvent.DisplayName, onDisplayNameChange);
|
||||
};
|
||||
}, [mx, userId]);
|
||||
|
||||
return profile;
|
||||
};
|
||||
16
src/app/hooks/useUserTrustStatusChange.ts
Normal file
16
src/app/hooks/useUserTrustStatusChange.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { CryptoEvent, CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { useEffect } from 'react';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export const useUserTrustStatusChange = (
|
||||
onChange: CryptoEventHandlerMap[CryptoEvent.UserTrustStatusChanged]
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
mx.on(CryptoEvent.UserTrustStatusChanged, onChange);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.UserTrustStatusChanged, onChange);
|
||||
};
|
||||
}, [mx, onChange]);
|
||||
};
|
||||
87
src/app/hooks/useVerificationRequest.ts
Normal file
87
src/app/hooks/useVerificationRequest.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
CryptoEvent,
|
||||
CryptoEventHandlerMap,
|
||||
VerificationPhase,
|
||||
VerificationRequest,
|
||||
VerificationRequestEvent,
|
||||
VerificationRequestEventHandlerMap,
|
||||
Verifier,
|
||||
VerifierEvent,
|
||||
VerifierEventHandlerMap,
|
||||
} from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
|
||||
export const useVerificationRequestReceived = (
|
||||
onRequest: CryptoEventHandlerMap[CryptoEvent.VerificationRequestReceived]
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useEffect(() => {
|
||||
mx.on(CryptoEvent.VerificationRequestReceived, onRequest);
|
||||
return () => {
|
||||
mx.removeListener(CryptoEvent.VerificationRequestReceived, onRequest);
|
||||
};
|
||||
}, [mx, onRequest]);
|
||||
};
|
||||
|
||||
export const useVerificationRequestChange = (
|
||||
request: VerificationRequest,
|
||||
onChange: VerificationRequestEventHandlerMap[VerificationRequestEvent.Change]
|
||||
) => {
|
||||
useEffect(() => {
|
||||
request.on(VerificationRequestEvent.Change, onChange);
|
||||
return () => {
|
||||
request.removeListener(VerificationRequestEvent.Change, onChange);
|
||||
};
|
||||
}, [request, onChange]);
|
||||
};
|
||||
|
||||
export const useVerificationRequestPhase = (request: VerificationRequest): VerificationPhase => {
|
||||
const [phase, setPhase] = useState(() => request.phase);
|
||||
|
||||
useVerificationRequestChange(
|
||||
request,
|
||||
useCallback(() => {
|
||||
setPhase(request.phase);
|
||||
}, [request])
|
||||
);
|
||||
|
||||
return phase;
|
||||
};
|
||||
|
||||
export const useVerifierCancel = (
|
||||
verifier: Verifier,
|
||||
onCallback: VerifierEventHandlerMap[VerifierEvent.Cancel]
|
||||
) => {
|
||||
useEffect(() => {
|
||||
verifier.on(VerifierEvent.Cancel, onCallback);
|
||||
return () => {
|
||||
verifier.removeListener(VerifierEvent.Cancel, onCallback);
|
||||
};
|
||||
}, [verifier, onCallback]);
|
||||
};
|
||||
|
||||
export const useVerifierShowSas = (
|
||||
verifier: Verifier,
|
||||
onCallback: VerifierEventHandlerMap[VerifierEvent.ShowSas]
|
||||
) => {
|
||||
useEffect(() => {
|
||||
verifier.on(VerifierEvent.ShowSas, onCallback);
|
||||
return () => {
|
||||
verifier.removeListener(VerifierEvent.ShowSas, onCallback);
|
||||
};
|
||||
}, [verifier, onCallback]);
|
||||
};
|
||||
|
||||
export const useVerifierShowReciprocateQr = (
|
||||
verifier: Verifier,
|
||||
onCallback: VerifierEventHandlerMap[VerifierEvent.ShowReciprocateQr]
|
||||
) => {
|
||||
useEffect(() => {
|
||||
verifier.on(VerifierEvent.ShowReciprocateQr, onCallback);
|
||||
return () => {
|
||||
verifier.removeListener(VerifierEvent.ShowReciprocateQr, onCallback);
|
||||
};
|
||||
}, [verifier, onCallback]);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue