From ffa1d9c48ce3a95b12719fb24985bd4de286aef6 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 17 Mar 2023 11:35:00 -0300 Subject: [PATCH] chore: ddp streamer typings (#28437) Co-authored-by: Tasso Evangelista --- .../lists/useStreamUpdatesForMessageList.ts | 11 +- .../providers/CallProvider/CallProvider.tsx | 8 +- .../client/providers/ServerProvider.tsx | 39 +++++ .../views/admin/import/ImportProgressPage.tsx | 1 - .../views/banners/hooks/useRemoteBanners.ts | 10 +- .../hooks/useThreadMainMessageQuery.ts | 24 +-- .../hooks/useVideoConfDataStream.ts | 71 ++------ .../src/ServerContext/ServerContext.ts | 8 + .../ui-contexts/src/ServerContext/methods.ts | 2 - .../ui-contexts/src/ServerContext/streams.ts | 164 ++++++++++++++++++ packages/ui-contexts/src/hooks/useStream.ts | 47 ++++- packages/ui-contexts/src/index.ts | 3 +- 12 files changed, 287 insertions(+), 101 deletions(-) create mode 100644 packages/ui-contexts/src/ServerContext/streams.ts diff --git a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts index 44e0aa6f517..c39b7982306 100644 --- a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts +++ b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts @@ -53,13 +53,10 @@ export const useStreamUpdatesForMessageList = (messageList: MessageList, uid: IU messageList.remove(mid); }); - const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom( - `${rid}/deleteMessageBulk`, - (params: NotifyRoomRidDeleteMessageBulkEvent) => { - const matchDeleteCriteria = createDeleteCriteria(params); - messageList.prune(matchDeleteCriteria); - }, - ); + const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom(`${rid}/deleteMessageBulk`, (params) => { + const matchDeleteCriteria = createDeleteCriteria(params); + messageList.prune(matchDeleteCriteria); + }); return (): void => { unsubscribeFromRoomMessages(); diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index 00e51210e76..bf5aea86588 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -271,7 +271,7 @@ export const CallProvider: FC = ({ children }) => { return; } - const handleCallHangup = (_event: { roomId: string }): void => { + return subscribeToNotifyUser(`${user._id}/call.hangup`, (event): void => { setQueueName(queueAggregator.getCurrentQueueName()); if (hasVoIPEnterpriseLicense) { @@ -281,10 +281,8 @@ export const CallProvider: FC = ({ children }) => { closeRoom(); - dispatchEvent({ event: VoipClientEvents['VOIP-CALL-ENDED'], rid: _event.roomId }); - }; - - return subscribeToNotifyUser(`${user._id}/call.hangup`, handleCallHangup); + dispatchEvent({ event: VoipClientEvents['VOIP-CALL-ENDED'], rid: event.roomId }); + }); }, [openWrapUpModal, queueAggregator, subscribeToNotifyUser, user, voipEnabled, dispatchEvent, hasVoIPEnterpriseLicense, closeRoom]); useEffect(() => { diff --git a/apps/meteor/client/providers/ServerProvider.tsx b/apps/meteor/client/providers/ServerProvider.tsx index 5f218d75597..15814dd63ed 100644 --- a/apps/meteor/client/providers/ServerProvider.tsx +++ b/apps/meteor/client/providers/ServerProvider.tsx @@ -1,4 +1,5 @@ import type { Serialized } from '@rocket.chat/core-typings'; +import { Emitter } from '@rocket.chat/emitter'; import type { Method, PathFor, OperationParams, OperationResult, UrlParams, PathPattern } from '@rocket.chat/rest-typings'; import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn, UploadResult } from '@rocket.chat/ui-contexts'; import { ServerContext } from '@rocket.chat/ui-contexts'; @@ -68,6 +69,43 @@ const getStream = ( }; }; +const ee = new Emitter>(); + +const events = new Map void>(); + +const getSingleStream = ( + streamName: string, +): ((eventName: string, callback: (...event: TEvent) => void) => () => void) => { + const stream = getStream(streamName); + return (eventName, callback): (() => void) => { + ee.on(`${streamName}/${eventName}`, callback); + + const handler = (...args: any[]): void => { + ee.emit(`${streamName}/${eventName}`, ...args); + }; + + const stop = (): void => { + // If someone is still listening, don't unsubscribe + ee.off(`${streamName}/${eventName}`, callback); + + if (ee.has(`${streamName}/${eventName}`)) { + return; + } + + const unsubscribe = events.get(`${streamName}/${eventName}`); + if (unsubscribe) { + unsubscribe(); + events.delete(`${streamName}/${eventName}`); + } + }; + + if (!events.has(`${streamName}/${eventName}`)) { + events.set(`${streamName}/${eventName}`, stream(eventName, handler)); + } + return stop; + }; +}; + const contextValue = { info, absoluteUrl, @@ -75,6 +113,7 @@ const contextValue = { callEndpoint, uploadToEndpoint, getStream, + getSingleStream, }; const ServerProvider: FC = ({ children }) => ; diff --git a/apps/meteor/client/views/admin/import/ImportProgressPage.tsx b/apps/meteor/client/views/admin/import/ImportProgressPage.tsx index d01fbe63d62..f2f962317e3 100644 --- a/apps/meteor/client/views/admin/import/ImportProgressPage.tsx +++ b/apps/meteor/client/views/admin/import/ImportProgressPage.tsx @@ -135,7 +135,6 @@ const ImportProgressPage = function ImportProgressPage() { useEffect(() => { return streamer('progress', ({ count: { completed, total }, ...rest }) => { - console.log('streamer', rest, completed, total); handleProgressUpdated({ ...rest, completed, total } as any); }); }, [handleProgressUpdated, streamer]); diff --git a/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts b/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts index be22c4b7b53..fdad9038984 100644 --- a/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts +++ b/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts @@ -40,7 +40,9 @@ export const useRemoteBanners = () => { }); }; - const handleBannerChange = async (event: { bannerId: string }): Promise => { + fetchInitialBanners(); + + const unsubscribeFromBannerChanged = subscribeToNotifyLoggedIn('banner-changed', async (event): Promise => { const response = await serverContext.callEndpoint({ method: 'GET', pathPattern: '/v1/banners/:id', @@ -59,11 +61,7 @@ export const useRemoteBanners = () => { response.banners.forEach((banner) => { banners.open(mapBanner(banner)); }); - }; - - fetchInitialBanners(); - - const unsubscribeFromBannerChanged = subscribeToNotifyLoggedIn('banner-changed', handleBannerChange); + }); return () => { controller.abort(); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts index 5c073d4ca01..14b07d38c32 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts @@ -13,8 +13,6 @@ import { useGetMessageByID } from './useGetMessageByID'; type RoomMessagesRidEvent = IMessage; -type NotifyRoomRidDeleteMessageEvent = { _id: IMessage['_id'] }; - type NotifyRoomRidDeleteMessageBulkEvent = { rid: IMessage['rid']; excludePinned: boolean; @@ -50,20 +48,14 @@ const useSubscribeToMessage = () => { if (message._id === event._id) onMutate?.(event); }); - const unsubscribeFromDeleteMessage = subscribeToNotifyRoom( - `${message.rid}/deleteMessage`, - (event: NotifyRoomRidDeleteMessageEvent) => { - if (message._id === event._id) onDelete?.(); - }, - ); - - const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom( - `${message.rid}/deleteMessageBulk`, - (params: NotifyRoomRidDeleteMessageBulkEvent) => { - const matchDeleteCriteria = createDeleteCriteria(params); - if (matchDeleteCriteria(message)) onDelete?.(); - }, - ); + const unsubscribeFromDeleteMessage = subscribeToNotifyRoom(`${message.rid}/deleteMessage`, (event) => { + if (message._id === event._id) onDelete?.(); + }); + + const unsubscribeFromDeleteMessageBulk = subscribeToNotifyRoom(`${message.rid}/deleteMessageBulk`, (params) => { + const matchDeleteCriteria = createDeleteCriteria(params); + if (matchDeleteCriteria(message)) onDelete?.(); + }); return () => { unsubscribeFromRoomMessages(); diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts index dad9d5795eb..116d8058f05 100644 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/hooks/useVideoConfDataStream.ts @@ -1,57 +1,9 @@ -import { useStream } from '@rocket.chat/ui-contexts'; -import { Emitter } from '@rocket.chat/emitter'; -import { useCallback, useEffect } from 'react'; +import { useSingleStream } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { useVideoConfData } from './useVideoConfData'; -const ee = new Emitter>(); - -const events = new Map void>(); - -const useStreamBySubPath = ( - streamer: ReturnType, - subpath: string, - callback: () => void -) => { - const maybeSubscribe = useCallback(() => { - // If we're already subscribed, don't do it again - if (events.has(subpath)) { - return; - } - - events.set( - subpath, - streamer(subpath, () => { - ee.emit(subpath); - }) - ); - }, [streamer, subpath]); - - const maybeUnsubscribe = useCallback(() => { - // If someone is still listening, don't unsubscribe - if (ee.has(subpath)) { - return; - } - - const unsubscribe = events.get(subpath); - if (unsubscribe) { - unsubscribe(); - events.delete(subpath); - } - }, [subpath]); - - useEffect(() => { - maybeSubscribe(); - ee.on(subpath, callback); - - return () => { - ee.off(subpath, callback); - maybeUnsubscribe(); - }; - }, [callback, subpath]); -}; - export const useVideoConfDataStream = ({ rid, callId, @@ -60,16 +12,17 @@ export const useVideoConfDataStream = ({ callId: string; }) => { const queryClient = useQueryClient(); - const subpath = `${rid}/${callId}`; - const subscribeNotifyRoom = useStream('notify-room'); + const subscribeNotifyRoom = useSingleStream('notify-room'); + + useEffect(() => { + return subscribeNotifyRoom( + `${rid}/videoconf`, + (id) => + id === callId && + queryClient.invalidateQueries(['video-conference', callId]) + ); + }, [rid, callId]); - useStreamBySubPath( - subscribeNotifyRoom, - subpath, - useCallback(() => { - queryClient.invalidateQueries(['video-conference', callId]); - }, [subpath]) - ); return useVideoConfData({ callId }); }; diff --git a/packages/ui-contexts/src/ServerContext/ServerContext.ts b/packages/ui-contexts/src/ServerContext/ServerContext.ts index 0180e93ea69..cc0a96f368c 100644 --- a/packages/ui-contexts/src/ServerContext/ServerContext.ts +++ b/packages/ui-contexts/src/ServerContext/ServerContext.ts @@ -38,6 +38,13 @@ export type ServerContextValue = { retransmitToSelf?: boolean | undefined; }, ) => (eventName: string, callback: (...event: TEvent) => void) => () => void; + getSingleStream: ( + streamName: string, + options?: { + retransmit?: boolean | undefined; + retransmitToSelf?: boolean | undefined; + }, + ) => (eventName: string, callback: (...event: TEvent) => void) => () => void; }; export const ServerContext = createContext({ @@ -50,4 +57,5 @@ export const ServerContext = createContext({ throw new Error('not implemented'); }, getStream: () => () => (): void => undefined, + getSingleStream: () => () => (): void => undefined, }); diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index 33c3e2dc534..b6aa916005a 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -1,7 +1,5 @@ import type { IMessage, IRoom, IMessageSearchProvider, IMessageSearchSuggestion, IUser } from '@rocket.chat/core-typings'; -// TODO: frontend chapter day - define methods - // eslint-disable-next-line @typescript-eslint/naming-convention export interface ServerMethods { 'addOAuthService': (...args: any[]) => any; diff --git a/packages/ui-contexts/src/ServerContext/streams.ts b/packages/ui-contexts/src/ServerContext/streams.ts new file mode 100644 index 00000000000..2527e57911d --- /dev/null +++ b/packages/ui-contexts/src/ServerContext/streams.ts @@ -0,0 +1,164 @@ +import type { + IMessage, + IRoom, + ISetting, + ISubscription, + IRole, + IEmoji, + ICustomSound, + INotificationDesktop, + IWebdavAccount, + VoipEventDataSignature, + IUser, + IOmnichannelRoom, + VideoConference, +} from '@rocket.chat/core-typings'; + +type StreamerKeyArgs = (key: K, cb: (...args: T) => void) => () => void; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface StreamerEvents { + 'roles': StreamerKeyArgs<'roles', [IRole]>; + + 'notify-room': StreamerKeyArgs<`${string}/user-activity`, [username: string, activities: string]> & + StreamerKeyArgs< + `${string}/deleteMessageBulk`, + [args: { rid: IMessage['rid']; excludePinned: boolean; ignoreDiscussion: boolean; ts: Record; users: string[] }] + > & + StreamerKeyArgs<`${string}/deleteMessage`, [{ _id: IMessage['_id'] }]> & + StreamerKeyArgs<`${string}/videoconf`, [id: string]>; + + 'room-messages': StreamerKeyArgs; + + 'notify-all': StreamerKeyArgs< + 'deleteEmojiCustom', + [ + { + emojiData: IEmoji; + }, + ] + > & + StreamerKeyArgs< + 'updateCustomSound', + [ + { + soundData: ICustomSound; + }, + ] + > & + StreamerKeyArgs<'public-settings-changed', ['inserted' | 'updated' | 'removed' | 'changed', ISetting]>; + + 'notify-user': StreamerKeyArgs<`${string}/rooms-changed`, [IRoom]> & + StreamerKeyArgs<`${string}/subscriptions-changed`, [ISubscription]> & + StreamerKeyArgs<`${string}/message`, [IMessage]> & + StreamerKeyArgs<`${string}/force_logout`, []> & + StreamerKeyArgs< + `${string}/webdav`, + [ + | { + type: 'changed'; + account: Partial; + } + | { + type: 'removed'; + account: { _id: IWebdavAccount['_id'] }; + }, + ] + > & + StreamerKeyArgs<`${string}/e2ekeyRequest`, [string, string]> & + StreamerKeyArgs<`${string}/notification`, [INotificationDesktop]> & + StreamerKeyArgs<`${string}/voip.events`, [VoipEventDataSignature]> & + StreamerKeyArgs< + `${string}/call.hangup`, + [ + { + roomId: string; + }, + ] + >; + + 'importers': StreamerKeyArgs<'progress', [{ rate: number; count: { completed: number; total: number } }]>; + + 'notify-logged': StreamerKeyArgs<'banner-changed', [{ bannerId: string }]> & + StreamerKeyArgs< + 'roles-change', + [ + { + type: 'added' | 'removed' | 'changed'; + _id: IRole['_id']; + u: { + _id: IUser['_id']; + username: IUser['username']; + name: IUser['name']; + }; + scope?: IRoom['_id']; + }, + ] + > & + StreamerKeyArgs<'Users:NameChanged', [Pick]> & + StreamerKeyArgs<'voip.statuschanged', [boolean]> & + StreamerKeyArgs<'omnichannel.priority-changed', [{ id: 'added' | 'removed' | 'changed'; name: string }]>; + + 'stdout': StreamerKeyArgs< + 'stdout', + [ + { + id: string; + string: string; + ts: Date; + }, + ] + >; + + 'room-data': StreamerKeyArgs; + 'notify-room-users': StreamerKeyArgs< + `${string}/video-conference`, + [ + { + action: string; + params: { + callId: VideoConference['_id']; + uid: IUser['_id']; + rid: IRoom['_id']; + }; + }, + ] + > & + StreamerKeyArgs<`${string}/webrtc`, unknown[]> & + StreamerKeyArgs<`${string}/otr`, unknown[]> & + StreamerKeyArgs<`${string}/userData`, unknown[]>; + + // 'notify-logged': ( + // e: + // | 'private-settings-changed' + // | 'permissions-changed' + // | 'roles-change' + // | 'deleteCustomUserStatus' + // | 'updateCustomUserStatus' + // | 'deleteEmojiCustom' + // | 'updateEmojiCustom' + // | 'updateAvatar' + // | 'Users:Deleted', + // ) => [void]; + + // 'apps': ( + // e: + // | 'app/added' + // | 'app/removed' + // | 'app/updated' + // | 'app/statusUpdate' + // | 'app/settingUpdate' + // | 'command/added' + // | 'command/disabled' + // | 'command/updated' + // | 'command/removed' + // | 'actions/changed', + // ) => [unknown]; + // 'user-presence': () => [void]; +} + +export type ServerStreamerNames = keyof StreamerEvents; + +export type ServerStreamerParameters = Parameters; + +export type ServerStreamerReturn = ReturnType; diff --git a/packages/ui-contexts/src/hooks/useStream.ts b/packages/ui-contexts/src/hooks/useStream.ts index 91613e3fb8e..7ee97cb770d 100644 --- a/packages/ui-contexts/src/hooks/useStream.ts +++ b/packages/ui-contexts/src/hooks/useStream.ts @@ -1,14 +1,53 @@ +import type { ServerStreamerNames, StreamerEvents } from '@rocket.chat/ui-contexts/src/ServerContext/streams'; import { useContext, useMemo } from 'react'; import { ServerContext } from '../ServerContext'; -export const useStream = ( - streamName: string, +export type ServerStreamFunction = ( + args: StreamerEvents[StreamName], + callback: (...args: any) => void, +) => () => void; + +export function useStream( + streamName: StreamName extends ServerStreamerNames ? never : StreamName, + options?: { + retransmit?: boolean; + retransmitToSelf?: boolean; + }, +): (key: string, cb: (...args: unknown[]) => void) => () => void; +export function useStream( + streamName: StreamName, + options?: { + retransmit?: boolean | undefined; + retransmitToSelf?: boolean | undefined; + }, +): StreamerEvents[StreamName]; +export function useStream( + streamName: StreamName, options?: { retransmit?: boolean | undefined; retransmitToSelf?: boolean | undefined; }, -): ((eventName: string, callback: (...event: TEvent) => void) => () => void) => { +): StreamerEvents[StreamName] { const { getStream } = useContext(ServerContext); return useMemo(() => getStream(streamName, options), [getStream, streamName, options]); -}; +} + +/* + * @param streamName The name of the stream to subscribe to + * @returns A function that can be used to subscribe to the stream + * the main difference between this and useStream is that this function + * will only subscribe to the `stream + key` only once, but you can still add multiple callbacks + * to the same path + */ + +export function useSingleStream( + streamName: StreamName, + options?: { + retransmit?: boolean | undefined; + retransmitToSelf?: boolean | undefined; + }, +): StreamerEvents[StreamName] { + const { getSingleStream } = useContext(ServerContext); + return useMemo(() => getSingleStream(streamName, options), [getSingleStream, streamName, options]); +} diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 658fa10e0b0..d6a18bd88c8 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -64,7 +64,7 @@ export { useSettings } from './hooks/useSettings'; export { useSettingsDispatch } from './hooks/useSettingsDispatch'; export { useSettingSetValue } from './hooks/useSettingSetValue'; export { useSettingStructure } from './hooks/useSettingStructure'; -export { useStream } from './hooks/useStream'; +export { useStream, useSingleStream } from './hooks/useStream'; export { useToastMessageDispatch } from './hooks/useToastMessageDispatch'; export { useTooltipClose } from './hooks/useTooltipClose'; export { useTooltipOpen } from './hooks/useTooltipOpen'; @@ -86,6 +86,7 @@ export { useSetOutputMediaDevice } from './hooks/useSetOutputMediaDevice'; export { useSetInputMediaDevice } from './hooks/useSetInputMediaDevice'; export { ServerMethods, ServerMethodName, ServerMethodParameters, ServerMethodReturn, ServerMethodFunction } from './ServerContext/methods'; +export { StreamerEvents } from './ServerContext/streams'; export { UploadResult } from './ServerContext'; export { TranslationKey, TranslationLanguage } from './TranslationContext'; export { Fields } from './UserContext';