Skip to content

Commit

Permalink
[IMPROVE] Message Collection Hooks (#20121)
Browse files Browse the repository at this point in the history
  • Loading branch information
tassoevan authored Jan 15, 2021
1 parent 28a2577 commit 4bfa63c
Show file tree
Hide file tree
Showing 33 changed files with 1,889 additions and 372 deletions.
13 changes: 9 additions & 4 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@
"indent": "off",
"no-extra-parens": "off",
"no-spaced-func": "off",
"no-unused-vars": "off",
"no-useless-constructor": "off",
"no-use-before-define": "off",
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/jsx-no-undef": "error",
Expand All @@ -99,6 +101,10 @@
"SwitchCase": 1
}
],
"@typescript-eslint/interface-name-prefix": [
"error",
"always"
],
"@typescript-eslint/no-extra-parens": [
"error",
"all",
Expand All @@ -111,10 +117,9 @@
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/interface-name-prefix": [
"error",
"always"
]
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_"
}]
},
"env": {
"browser": true,
Expand Down
1 change: 1 addition & 0 deletions app/ui-utils/client/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';

const url = new URL(window.location);
const keys = new Set();

export const getConfig = (key) => {
keys.add(key);
return url.searchParams.get(key) || Meteor._localStorage.getItem(`rc-config-${ key }`);
Expand Down
17 changes: 6 additions & 11 deletions client/contexts/ServerContext.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { createContext, useCallback, useContext, useMemo } from 'react';

interface IServerStream {
on(eventName: string, callback: (data: any) => void): void;
off(eventName: string, callback: (data: any) => void): void;
}

type ServerContextValue = {
info: {};
absoluteUrl: (path: string) => string;
callMethod: (methodName: string, ...args: any[]) => Promise<any>;
callEndpoint: (httpMethod: 'GET' | 'POST' | 'DELETE', endpoint: string, ...args: any[]) => Promise<any>;
uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise<void>;
getStream: (streamName: string, options?: {}) => IServerStream;
getStream: (streamName: string, options?: {}) => <T>(eventName: string, callback: (data: T) => void) => () => void;
};

export const ServerContext = createContext<ServerContextValue>({
Expand All @@ -20,10 +15,7 @@ export const ServerContext = createContext<ServerContextValue>({
callMethod: async () => undefined,
callEndpoint: async () => undefined,
uploadToEndpoint: async () => undefined,
getStream: () => ({
on: (): void => undefined,
off: (): void => undefined,
}),
getStream: () => () => (): void => undefined,
});

export const useServerInformation = (): {} => useContext(ServerContext).info;
Expand All @@ -45,7 +37,10 @@ export const useUpload = (endpoint: string): (params: any, formData: any) => Pro
return useCallback((params, formData: any) => uploadToEndpoint(endpoint, params, formData), [endpoint, uploadToEndpoint]);
};

export const useStream = (streamName: string, options?: {}): IServerStream => {
export const useStream = (
streamName: string,
options?: {},
): <T>(eventName: string, callback: (data: T) => void) => (() => void) => {
const { getStream } = useContext(ServerContext);
return useMemo(() => getStream(streamName, options), [getStream, streamName, options]);
};
65 changes: 65 additions & 0 deletions client/hooks/lists/useRecordList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useEffect, useState } from 'react';

import { AsyncStatePhase } from '../../lib/asyncState';
import { RecordList } from '../../lib/lists/RecordList';
import { IRocketChatRecord } from '../../../definition/IRocketChatRecord';

type RecordListValue<T> = {
phase: AsyncStatePhase;
items: T[];
itemCount: number;
error: Error | undefined;
}

export const useRecordList = <T extends IRocketChatRecord>(
recordList: RecordList<T>,
): RecordListValue<T> => {
const [state, setState] = useState<RecordListValue<T>>(() => ({
phase: recordList.phase,
items: recordList.items,
itemCount: recordList.itemCount,
error: undefined,
}));

useEffect(() => {
const disconnectMutatingEvent = recordList.on('mutating', () => {
setState(() => ({
phase: recordList.phase,
items: recordList.items,
itemCount: recordList.itemCount,
error: undefined,
}));
});

const disconnectMutatedEvent = recordList.on('mutated', () => {
setState((prevState) => ({
phase: recordList.phase,
items: recordList.items,
itemCount: recordList.itemCount,
error: prevState.error,
}));
});

const disconnectClearedEvent = recordList.on('cleared', () => {
setState(() => ({
phase: recordList.phase,
items: recordList.items,
itemCount: recordList.itemCount,
error: undefined,
}));
});

const disconnectErroredEvent = recordList.on('errored', (error) => {
setState((state) => ({ ...state, error }));
});

return (): void => {
disconnectMutatingEvent();
disconnectMutatedEvent();
disconnectClearedEvent();
disconnectErroredEvent();
};
}, [recordList]);

return state;
};
30 changes: 30 additions & 0 deletions client/hooks/lists/useScrollableMessageList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useCallback } from 'react';

import { IMessage } from '../../../definition/IMessage';
import { MessageList } from '../../lib/lists/MessageList';
import { ObjectFromApi } from '../../../definition/ObjectFromApi';
import { useScrollableRecordList } from './useScrollableRecordList';
import { RecordListBatchChanges } from '../../lib/lists/RecordList';

const convertMessageFromApi = (apiMessage: ObjectFromApi<IMessage>): IMessage => ({
...apiMessage,
_updatedAt: new Date(apiMessage._updatedAt),
ts: new Date(apiMessage.ts),
...apiMessage.tlm && { tlm: new Date(apiMessage.tlm) },
});

export const useScrollableMessageList = (
messageList: MessageList,
fetchMessages: (start: number, end: number) => Promise<RecordListBatchChanges<ObjectFromApi<IMessage>>>,
initialItemCount?: number,
): ReturnType<typeof useScrollableRecordList> => {
const fetchItems = useCallback(async (start: number, end: number): Promise<RecordListBatchChanges<IMessage>> => {
const batchChanges = await fetchMessages(start, end);
return {
...batchChanges.items && { items: batchChanges.items.map(convertMessageFromApi) },
...batchChanges.itemCount && { itemCount: batchChanges.itemCount },
};
}, [fetchMessages]);

return useScrollableRecordList(messageList, fetchItems, initialItemCount);
};
28 changes: 28 additions & 0 deletions client/hooks/lists/useScrollableRecordList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useCallback, useEffect } from 'react';

import { RecordList, RecordListBatchChanges } from '../../lib/lists/RecordList';
import { IRocketChatRecord } from '../../../definition/IRocketChatRecord';

const INITIAL_ITEM_COUNT = 25;

export const useScrollableRecordList = <T extends IRocketChatRecord>(
recordList: RecordList<T>,
fetchBatchChanges: (start: number, end: number) => Promise<RecordListBatchChanges<T>>,
initialItemCount: number = INITIAL_ITEM_COUNT,
): {
loadMoreItems: (start: number, end: number) => void;
initialItemCount: number;
} => {
const loadMoreItems = useCallback(
(start: number, end: number) => {
recordList.batchHandle(() => fetchBatchChanges(start, end));
},
[recordList, fetchBatchChanges],
);

useEffect(() => {
loadMoreItems(0, initialItemCount ?? INITIAL_ITEM_COUNT);
}, [loadMoreItems, initialItemCount]);

return { loadMoreItems, initialItemCount };
};
80 changes: 80 additions & 0 deletions client/hooks/lists/useStreamUpdatesForMessageList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect } from 'react';

import { useStream } from '../../contexts/ServerContext';
import { IMessage } from '../../../definition/IMessage';
import {
createFilterFromQuery,
FieldExpression,
Query,
} from '../../lib/minimongo';
import { MessageList } from '../../lib/lists/MessageList';
import { IRoom } from '../../../definition/IRoom';
import { IUser } from '../../../definition/IUser';

type RoomMessagesRidEvent = IMessage;

type NotifyRoomRidDeleteMessageEvent = { _id: IMessage['_id'] };

type NotifyRoomRidDeleteMessageBulkEvent = {
rid: IMessage['rid'];
excludePinned: boolean;
ignoreDiscussion: boolean;
ts: FieldExpression<Date>;
users: string[];
};

const createDeleteCriteria = (
params: NotifyRoomRidDeleteMessageBulkEvent,
): ((message: IMessage) => boolean) => {
const query: Query<IMessage> = { ts: params.ts };

if (params.excludePinned) {
query.pinned = { $ne: true };
}

if (params.ignoreDiscussion) {
query.drid = { $exists: false };
}
if (params.users && params.users.length) {
query['u.username'] = { $in: params.users };
}

return createFilterFromQuery<IMessage>(query);
};

export const useStreamUpdatesForMessageList = (messageList: MessageList, uid: IUser['_id'] | null, rid: IRoom['_id'] | null): void => {
const subscribeToRoomMessages = useStream('room-messages');
const subscribeToNotifyRoom = useStream('notify-room');

useEffect(() => {
if (!uid || !rid) {
messageList.clear();
return;
}

const unsubscribeFromRoomMessages = subscribeToRoomMessages<RoomMessagesRidEvent>(rid, (message) => {
messageList.handle(message);
});

const unsubscribeFromDeleteMessage = subscribeToNotifyRoom<NotifyRoomRidDeleteMessageEvent>(
`${ rid }/deleteMessage`,
({ _id: mid }) => {
messageList.remove(mid);
},
);

const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom<NotifyRoomRidDeleteMessageBulkEvent>(
`${ rid }/deleteMessageBulk`,
(params) => {
const matchDeleteCriteria = createDeleteCriteria(params);
messageList.prune(matchDeleteCriteria);
},
);

return (): void => {
unsubscribeFromRoomMessages();
unsubscribeFromDeleteMessage();
unsubscribeFromDeleteMessageBulk();
};
}, [subscribeToRoomMessages, subscribeToNotifyRoom, uid, rid, messageList]);
};
Loading

0 comments on commit 4bfa63c

Please sign in to comment.