diff --git a/client/contexts/UserContext.ts b/client/contexts/UserContext.ts index c59596f9b5300..5c7ef3548781f 100644 --- a/client/contexts/UserContext.ts +++ b/client/contexts/UserContext.ts @@ -32,7 +32,7 @@ type UserContextValue = { loginWithPassword: (user: string | object, password: string) => Promise; queryPreference: (key: string | Mongo.ObjectID, defaultValue?: T) => Subscription; querySubscription: (query: FilterQuery, fields: Fields, sort?: Sort) => Subscription ; - queryRoom: (query: FilterQuery, fields: Fields, sort?: Sort) => Subscription ; + queryRoom: (query: FilterQuery, fields?: Fields, sort?: Sort) => Subscription ; querySubscriptions: (query: SubscriptionQuery, options?: FindOptions) => Subscription | []>; }; @@ -79,7 +79,7 @@ export const useUserSubscription = (rid: string, fields: Fields): ISubscription return useSubscription(subscription); }; -export const useUserRoom = (rid: string, fields: Fields): IRoom | undefined => { +export const useUserRoom = (rid: string, fields?: Fields): IRoom | undefined => { const { queryRoom } = useContext(UserContext); const subscription = useMemo(() => queryRoom({ _id: rid }, fields), [queryRoom, rid, fields]); return useSubscription(subscription); diff --git a/client/lib/lists/FilesList.ts b/client/lib/lists/FilesList.ts new file mode 100644 index 0000000000000..1e0cb830f328e --- /dev/null +++ b/client/lib/lists/FilesList.ts @@ -0,0 +1,42 @@ +import { MessageList } from './MessageList'; +import type { IMessage } from '../../../definition/IMessage'; + +type FilesMessage = Omit & Required>; + +export type FilesListOptions = { + rid: IMessage['rid']; + type: string; + text: string; +}; + +const isFileMessageInRoom = (message: IMessage, rid: IMessage['rid']): message is FilesMessage => + message.rid === rid && 'rid' in message; + +export class FilesList extends MessageList { + public constructor(private _options: FilesListOptions) { + super(); + } + + public get options(): FilesListOptions { + return this._options; + } + + public updateFilters(options: FilesListOptions): void { + this._options = options; + this.clear(); + } + + protected filter(message: IMessage): boolean { + const { rid } = this._options; + + if (!isFileMessageInRoom(message, rid)) { + return false; + } + + return true; + } + + protected compare(a: IMessage, b: IMessage): number { + return (b.tlm ?? b.ts).getTime() - (a.tlm ?? a.ts).getTime(); + } +} diff --git a/client/views/room/contextualBar/RoomFiles/RoomFiles.js b/client/views/room/contextualBar/RoomFiles/RoomFiles.js index eb57e6ecb1550..5e7c4eeb45cc7 100644 --- a/client/views/room/contextualBar/RoomFiles/RoomFiles.js +++ b/client/views/room/contextualBar/RoomFiles/RoomFiles.js @@ -1,5 +1,5 @@ import React, { useState, useCallback, useMemo } from 'react'; -import { useMutableCallback, useLocalStorage, useDebouncedState, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useMutableCallback, useLocalStorage, useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Box, Icon, @@ -11,7 +11,7 @@ import { import { Virtuoso } from 'react-virtuoso'; import memoize from 'memoize-one'; -import { useUserId, useUserRoom } from '../../../../contexts/UserContext'; +import { useUserId } from '../../../../contexts/UserContext'; import DeleteFileWarning from '../../../../components/DeleteFileWarning'; import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext'; import { useSetModal } from '../../../../contexts/ModalContext'; @@ -20,8 +20,8 @@ import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; import { useTranslation } from '../../../../contexts/TranslationContext'; import VerticalBar from '../../../../components/VerticalBar'; import FileItem from './components/FileItem'; -import { useFileList } from './hooks/useFileList'; -import { useComponentDidUpdate } from '../../../../hooks/useComponentDidUpdate'; +import { useFilesList } from './hooks/useFilesList'; +import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { useMessageDeletionIsAllowed } from './hooks/useMessageDeletionIsAllowed'; import { useTabBarClose } from '../../providers/ToolboxProvider'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; @@ -79,8 +79,6 @@ export const RoomFiles = function RoomFiles({ const itemData = createItemData(onClickDelete, isDeletionAllowed); - const lm = useMutableCallback((start) => loadMoreItems(start + 1, Math.min(50, start + 1 - filesItems.length))); - return ( <> @@ -111,7 +109,7 @@ export const RoomFiles = function RoomFiles({ {} : lm} + endReached={loading ? () => {} : loadMoreItems} overscan={50} data={filesItems} components={{ Scroller: ScrollableContentWrapper }} @@ -132,9 +130,6 @@ RoomFiles.Item = FileItem; export default ({ rid }) => { const uid = useUserId(); const onClickClose = useTabBarClose(); - const room = useUserRoom(rid); - room.type = room.t; - room.rid = rid; const setModal = useSetModal(); const closeModal = useMutableCallback(() => setModal()); @@ -144,33 +139,12 @@ export default ({ rid }) => { const [type, setType] = useLocalStorage('file-list-type', 'all'); const [text, setText] = useState(''); - const [query, setQuery] = useDebouncedState({ - roomId: rid, - sort: JSON.stringify({ uploadedAt: -1 }), - count: 50, - query: JSON.stringify({ - ...type !== 'all' && { - typeGroup: type, - }, - }), - }, 500); - const handleTextChange = useCallback((event) => { setText(event.currentTarget.value); }, []); - useComponentDidUpdate(() => setQuery((params) => ({ - ...params, - roomId: rid, - query: JSON.stringify({ - name: { $regex: text || '', $options: 'i' }, - ...type !== 'all' && { - typeGroup: type, - }, - }), - })), [rid, text, type, setQuery]); - - const { value: data, phase: state, reload, more } = useFileList(room.type, query); + const { filesList, loadMoreItems } = useFilesList(useMemo(() => ({ rid, type, text }), [rid, type, text])); + const { phase, items: filesItems, itemCount: totalItemCount } = useRecordList(filesList); const handleDelete = useMutableCallback((_id) => { const onConfirm = async () => { @@ -180,38 +154,24 @@ export default ({ rid }) => { dispatchToastMessage({ type: 'error', message: error }); } closeModal(); - reload(); }; setModal(); }, []); - const loadMoreItems = useCallback( - (start, end) => { - more( - (params) => ({ ...params, offset: start, count: end - start }), - (prev, next) => ({ - total: next.total, - files: [...prev.files, ...next.files], - }), - ); - }, - [more], - ); - const isDeletionAllowed = useMessageDeletionIsAllowed(rid, uid); return ( : } {name} - @{user.username} + @{user?.username} {format(uploadedAt)} - + ; }; diff --git a/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts b/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts new file mode 100644 index 0000000000000..7e6b2a44fe5e0 --- /dev/null +++ b/client/views/room/contextualBar/RoomFiles/hooks/useFilesList.ts @@ -0,0 +1,79 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { + FilesList, + FilesListOptions, +} from '../../../../../lib/lists/FilesList'; +import { useEndpoint } from '../../../../../contexts/ServerContext'; +import { useUserRoom, useUserId } from '../../../../../contexts/UserContext'; +import { useScrollableMessageList } from '../../../../../hooks/lists/useScrollableMessageList'; +import { useStreamUpdatesForMessageList } from '../../../../../hooks/lists/useStreamUpdatesForMessageList'; +import { getConfig } from '../../../../../../app/ui-utils/client/config'; + +export const useFilesList = ( + options: FilesListOptions, +): { + filesList: FilesList; + initialItemCount: number; + loadMoreItems: (start: number, end: number) => void; + } => { + const [filesList] = useState(() => new FilesList(options)); + + const room = useUserRoom(options.rid); + const uid = useUserId(); + + useEffect(() => { + if (filesList.options !== options) { + filesList.updateFilters(options); + } + }, [filesList, options]); + + const roomTypes = { + c: 'channels.files', + l: 'channels.files', + d: 'im.files', + p: 'groups.files', + }; + + const apiEndPoint = room && roomTypes[room.t]; + + const getFiles = useEndpoint('GET', apiEndPoint as string); + + const fetchMessages = useCallback( + async (start, end) => { + const { files, total } = await getFiles({ + roomId: options.rid, + count: end - start, + sort: JSON.stringify({ uploadedAt: -1 }), + query: JSON.stringify({ + name: { $regex: options.text || '', $options: 'i' }, + ...options.type !== 'all' && { + typeGroup: options.type, + }, + }), + }); + + return { + items: files, + itemCount: total, + }; + }, + [getFiles, options.rid, options.type, options.text], + ); + + const { loadMoreItems, initialItemCount } = useScrollableMessageList( + filesList, + fetchMessages, + useMemo(() => { + const filesListSize = getConfig('discussionListSize'); + return filesListSize ? parseInt(filesListSize, 10) : undefined; + }, []), + ); + useStreamUpdatesForMessageList(filesList, uid, options.rid); + + return { + filesList, + loadMoreItems, + initialItemCount, + }; +};