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:
Ajay Bura 2023-06-22 09:14:50 +10:00 committed by GitHub
parent da32d0d9e7
commit c07905c360
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 984 additions and 79 deletions

View file

@ -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];
};

View 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;
};

View 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]
);
};

View file

@ -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;
};

View file

@ -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;
};

View 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;
};