mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-06 15:30:27 +03:00
Improve Members Right Panel (#1286)
* fix room members hook * fix resize observer hook * add intersection observer hook * install react-virtual lib * improve right panel - WIP * add filters for members * fix bug in async search * categories members and add search * show spinner on room member fetch * make invite member btn clickable * so no member text * add line between room view and member drawer * fix imports * add screen size hook * fix set setting hook * make member drawer responsive * extract power level tags hook * fix room members hook * fix use async search api * produce search result on filter change
This commit is contained in:
parent
da32d0d9e7
commit
c07905c360
19 changed files with 984 additions and 79 deletions
|
|
@ -25,11 +25,13 @@ export type UseAsyncSearchResult<TSearchItem extends object | string | number> =
|
|||
items: TSearchItem[];
|
||||
};
|
||||
|
||||
export type SearchResetHandler = () => void;
|
||||
|
||||
export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
||||
list: TSearchItem[],
|
||||
getItemStr: SearchItemStrGetter<TSearchItem>,
|
||||
options?: UseAsyncSearchOptions
|
||||
): [UseAsyncSearchResult<TSearchItem> | undefined, AsyncSearchHandler] => {
|
||||
): [UseAsyncSearchResult<TSearchItem> | undefined, AsyncSearchHandler, SearchResetHandler] => {
|
||||
const [result, setResult] = useState<UseAsyncSearchResult<TSearchItem>>();
|
||||
|
||||
const [searchCallback, terminateSearch] = useMemo(() => {
|
||||
|
|
@ -51,7 +53,7 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
|||
const handleResult: ResultHandler<TSearchItem> = (results, query) =>
|
||||
setResult({
|
||||
query,
|
||||
items: results,
|
||||
items: [...results],
|
||||
});
|
||||
|
||||
return AsyncSearch(list, handleMatch, handleResult, options);
|
||||
|
|
@ -60,15 +62,16 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
|||
const searchHandler: AsyncSearchHandler = useCallback(
|
||||
(query) => {
|
||||
const normalizedQuery = normalize(query, options?.normalizeOptions);
|
||||
if (!normalizedQuery) {
|
||||
setResult(undefined);
|
||||
return;
|
||||
}
|
||||
searchCallback(normalizedQuery);
|
||||
},
|
||||
[searchCallback, options?.normalizeOptions]
|
||||
);
|
||||
|
||||
const resetHandler: SearchResetHandler = useCallback(() => {
|
||||
terminateSearch();
|
||||
setResult(undefined);
|
||||
}, [terminateSearch]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// terminate any ongoing search request on unmount.
|
||||
|
|
@ -77,5 +80,5 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
|
|||
[terminateSearch]
|
||||
);
|
||||
|
||||
return [result, searchHandler];
|
||||
return [result, searchHandler, resetHandler];
|
||||
};
|
||||
|
|
|
|||
37
src/app/hooks/useIntersectionObserver.ts
Normal file
37
src/app/hooks/useIntersectionObserver.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export type OnIntersectionCallback = (entries: IntersectionObserverEntry[]) => void;
|
||||
|
||||
export type IntersectionObserverOpts = {
|
||||
root?: Element | Document | null;
|
||||
rootMargin?: string;
|
||||
threshold?: number | number[];
|
||||
};
|
||||
|
||||
export const getIntersectionObserverEntry = (
|
||||
target: Element | Document,
|
||||
entries: IntersectionObserverEntry[]
|
||||
): IntersectionObserverEntry | undefined => entries.find((entry) => entry.target === target);
|
||||
|
||||
export const useIntersectionObserver = (
|
||||
onIntersectionCallback: OnIntersectionCallback,
|
||||
opts?: IntersectionObserverOpts | (() => IntersectionObserverOpts),
|
||||
observeElement?: Element | null | (() => Element | null)
|
||||
): IntersectionObserver | undefined => {
|
||||
const [intersectionObserver, setIntersectionObserver] = useState<IntersectionObserver>();
|
||||
|
||||
useEffect(() => {
|
||||
const initOpts = typeof opts === 'function' ? opts() : opts;
|
||||
setIntersectionObserver(new IntersectionObserver(onIntersectionCallback, initOpts));
|
||||
}, [onIntersectionCallback, opts]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = typeof observeElement === 'function' ? observeElement() : observeElement;
|
||||
if (element) intersectionObserver?.observe(element);
|
||||
return () => {
|
||||
if (element) intersectionObserver?.unobserve(element);
|
||||
};
|
||||
}, [intersectionObserver, observeElement]);
|
||||
|
||||
return intersectionObserver;
|
||||
};
|
||||
38
src/app/hooks/usePowerLevelTags.ts
Normal file
38
src/app/hooks/usePowerLevelTags.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export type PowerLevelTag = {
|
||||
name: string;
|
||||
};
|
||||
export const usePowerLevelTags = () => {
|
||||
const powerLevelTags = useMemo(
|
||||
() => ({
|
||||
9000: {
|
||||
name: 'Goku',
|
||||
},
|
||||
101: {
|
||||
name: 'Founder',
|
||||
},
|
||||
100: {
|
||||
name: 'Admin',
|
||||
},
|
||||
50: {
|
||||
name: 'Moderator',
|
||||
},
|
||||
0: {
|
||||
name: 'Default',
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return useCallback(
|
||||
(powerLevel: number): PowerLevelTag => {
|
||||
if (powerLevel >= 9000) return powerLevelTags[9000];
|
||||
if (powerLevel >= 101) return powerLevelTags[101];
|
||||
if (powerLevel === 100) return powerLevelTags[100];
|
||||
if (powerLevel >= 50) return powerLevelTags[50];
|
||||
return powerLevelTags[0];
|
||||
},
|
||||
[powerLevelTags]
|
||||
);
|
||||
};
|
||||
|
|
@ -8,17 +8,18 @@ export const getResizeObserverEntry = (
|
|||
): ResizeObserverEntry | undefined => entries.find((entry) => entry.target === target);
|
||||
|
||||
export const useResizeObserver = (
|
||||
element: Element | null,
|
||||
onResizeCallback: OnResizeCallback
|
||||
onResizeCallback: OnResizeCallback,
|
||||
observeElement?: Element | null | (() => Element | null)
|
||||
): ResizeObserver => {
|
||||
const resizeObserver = useMemo(() => new ResizeObserver(onResizeCallback), [onResizeCallback]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = typeof observeElement === 'function' ? observeElement() : observeElement;
|
||||
if (element) resizeObserver.observe(element);
|
||||
return () => {
|
||||
if (element) resizeObserver.unobserve(element);
|
||||
};
|
||||
}, [resizeObserver, element]);
|
||||
}, [resizeObserver, observeElement]);
|
||||
|
||||
return resizeObserver;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
import { MatrixClient, MatrixEvent, RoomMember, RoomMemberEvent } from 'matrix-js-sdk';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAlive } from './useAlive';
|
||||
|
||||
export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] => {
|
||||
const [members, setMembers] = useState<RoomMember[]>([]);
|
||||
const alive = useAlive();
|
||||
|
||||
useEffect(() => {
|
||||
const room = mx.getRoom(roomId);
|
||||
let loadingMembers = true;
|
||||
let disposed = false;
|
||||
|
||||
const updateMemberList = (event?: MatrixEvent) => {
|
||||
if (!room || !alive || (event && event.getRoomId() !== roomId)) return;
|
||||
if (!room || disposed || (event && event.getRoomId() !== roomId)) return;
|
||||
if (loadingMembers) return;
|
||||
setMembers(room.getMembers());
|
||||
};
|
||||
|
||||
if (room) {
|
||||
updateMemberList();
|
||||
setMembers(room.getMembers());
|
||||
room.loadMembersIfNeeded().then(() => {
|
||||
if (!alive) return;
|
||||
loadingMembers = false;
|
||||
if (disposed) return;
|
||||
updateMemberList();
|
||||
});
|
||||
}
|
||||
|
|
@ -25,10 +27,11 @@ export const useRoomMembers = (mx: MatrixClient, roomId: string): RoomMember[] =
|
|||
mx.on(RoomMemberEvent.Membership, updateMemberList);
|
||||
mx.on(RoomMemberEvent.PowerLevel, updateMemberList);
|
||||
return () => {
|
||||
disposed = true;
|
||||
mx.removeListener(RoomMemberEvent.Membership, updateMemberList);
|
||||
mx.removeListener(RoomMemberEvent.PowerLevel, updateMemberList);
|
||||
};
|
||||
}, [mx, roomId, alive]);
|
||||
}, [mx, roomId]);
|
||||
|
||||
return members;
|
||||
};
|
||||
|
|
|
|||
36
src/app/hooks/useScreenSize.ts
Normal file
36
src/app/hooks/useScreenSize.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import { getResizeObserverEntry, useResizeObserver } from './useResizeObserver';
|
||||
|
||||
export const TABLET_BREAKPOINT = 1124;
|
||||
export const MOBILE_BREAKPOINT = 750;
|
||||
|
||||
export enum ScreenSize {
|
||||
Desktop = 'Desktop',
|
||||
Tablet = 'Tablet',
|
||||
Mobile = 'Mobile',
|
||||
}
|
||||
|
||||
export const getScreenSize = (width: number): ScreenSize => {
|
||||
if (width > TABLET_BREAKPOINT) return ScreenSize.Desktop;
|
||||
if (width > MOBILE_BREAKPOINT) return ScreenSize.Tablet;
|
||||
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
|
||||
);
|
||||
|
||||
return size;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue