From 7dba7b8b76cd6ba898edd38f0e1e858c129a03e9 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 15 Dec 2022 16:02:21 -0300 Subject: [PATCH] Migrate thread template to component --- .../app/threads/client/flextab/thread.html | 22 - .../app/threads/client/flextab/thread.ts | 316 -------------- .../app/threads/client/flextab/threads.ts | 1 - apps/meteor/app/threads/client/index.ts | 3 +- .../body/LegacyMessageTemplateList.tsx | 12 +- ...ageContext.ts => useRoomMessageContext.ts} | 2 +- .../Threads/components/ThreadChat.tsx | 396 +++++++++++++++--- .../page-objects/fragments/home-flextab.ts | 2 +- packages/ui-contexts/package.json | 3 + .../ui-contexts/src/ServerContext/methods.ts | 1 + 10 files changed, 347 insertions(+), 411 deletions(-) delete mode 100644 apps/meteor/app/threads/client/flextab/thread.html delete mode 100644 apps/meteor/app/threads/client/flextab/thread.ts delete mode 100644 apps/meteor/app/threads/client/flextab/threads.ts rename apps/meteor/client/views/room/components/body/{useMessageContext.ts => useRoomMessageContext.ts} (98%) diff --git a/apps/meteor/app/threads/client/flextab/thread.html b/apps/meteor/app/threads/client/flextab/thread.html deleted file mode 100644 index e67da765ae816..0000000000000 --- a/apps/meteor/app/threads/client/flextab/thread.html +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/apps/meteor/app/threads/client/flextab/thread.ts b/apps/meteor/app/threads/client/flextab/thread.ts deleted file mode 100644 index cd7e77cceb096..0000000000000 --- a/apps/meteor/app/threads/client/flextab/thread.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; -import { Template } from 'meteor/templating'; -import { Session } from 'meteor/session'; -import { ReactiveDict } from 'meteor/reactive-dict'; -import { Tracker } from 'meteor/tracker'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import type { IMessage, IEditedMessage, ISubscription, IRoom, IThreadMainMessage } from '@rocket.chat/core-typings'; -import type { ContextType } from 'react'; - -import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; -import { messageContext } from '../../../ui-utils/client/lib/messageContext'; -import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; -import { Messages } from '../../../models/client'; -import { getUserPreference } from '../../../utils/client'; -import { callbacks } from '../../../../lib/callbacks'; -import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; -import type { MessageContext } from '../../../../client/views/room/contexts/MessageContext'; -import type { ChatContext } from '../../../../client/views/room/contexts/ChatContext'; -import type MessageHighlightContext from '../../../../client/views/room/MessageList/contexts/MessageHighlightContext'; -import { withDebouncing, withThrottling } from '../../../../lib/utils/highOrderFunctions'; -import './thread.html'; - -export type ThreadTemplateInstance = Blaze.TemplateInstance<{ - mainMessage: IThreadMainMessage; - subscription?: ISubscription; - rid: IRoom['_id']; - tabBar: { - openRoomInfo: (username: string) => void; - }; - chatContext: ContextType; - messageContext: ContextType; - messageHighlightContext: ContextType; - sendToChannel: boolean; - onSend?: () => void; -}> & { - firstNode: HTMLElement; - wrapper?: HTMLElement; - Threads: Mongo.Collection, IMessage> & { - direct: Mongo.Collection, IMessage>; - queries: unknown[]; - }; - threadsObserve?: Meteor.LiveQueryHandle; - callbackRemove?: () => void; - state: ReactiveDict<{ - rid: string; - tmid?: string; - loading?: boolean; - jump?: string | null; - editingMID?: IMessage['_id']; - }>; - closeThread: () => void; - loadMore: () => Promise; - atBottom?: boolean; - sendToBottom: () => void; - sendToBottomIfNecessary: () => void; - lastJump?: string; -}; - -const sort = { ts: 1 }; - -Template.thread.events({ - ...getCommonRoomEvents(), - 'scroll .js-scroll-thread': withThrottling({ wait: 150 })(({ currentTarget: e }: JQuery.ScrollEvent, i: ThreadTemplateInstance) => { - i.atBottom = e.scrollTop >= e.scrollHeight - e.clientHeight; - }), - 'click .toggle-hidden'(e: JQuery.ClickEvent) { - const id = e.currentTarget.dataset.message; - document.querySelector(`#thread-${id}`)?.classList.toggle('message--ignored'); - }, -}); - -Template.thread.helpers({ - mainMessage() { - const { Threads, state } = Template.instance() as ThreadTemplateInstance; - const tmid = state.get('tmid'); - return Threads.findOne({ _id: tmid }); - }, - isLoading() { - return (Template.instance() as ThreadTemplateInstance).state.get('loading') !== false; - }, - messages() { - const { Threads, state } = Template.instance() as ThreadTemplateInstance; - const tmid = state.get('tmid'); - - return Threads.find({ tmid, _id: { $ne: tmid } }, { sort }); - }, - customClass(msg: IMessage) { - const { state } = Template.instance() as ThreadTemplateInstance; - return msg._id === state.get('editingMID') ? 'editing' : ''; - }, - customClassMain() { - const { state } = Template.instance() as ThreadTemplateInstance; - return ['thread-main', state.get('tmid') === state.get('editingMID') ? 'editing' : ''].filter(Boolean).join(' '); - }, - _messageContext(this: ThreadTemplateInstance['data']) { - const result = messageContext.call(this, { rid: this.mainMessage?.rid }); - return { - ...result, - settings: { - ...result.settings, - showReplyButton: false, - showreply: false, - }, - }; - }, - hideUsername() { - return getUserPreference(Meteor.userId(), 'hideUsernames') ? 'hide-usernames' : undefined; - }, - // TODO: remove this - chatContext() { - const { chatContext } = (Template.instance() as ThreadTemplateInstance).data; - return () => chatContext; - }, - // TODO: remove this - messageContext() { - const { messageContext } = (Template.instance() as ThreadTemplateInstance).data; - return () => messageContext; - }, -}); - -Template.thread.onCreated(async function (this: ThreadTemplateInstance) { - this.Threads = new Mongo.Collection, IMessage>(null) as Mongo.Collection, IMessage> & { - direct: Mongo.Collection, IMessage>; - queries: unknown[]; - }; - - const preferenceState = getUserPreference(Meteor.userId(), 'alsoSendThreadToChannel'); - - let sendToChannel; - switch (preferenceState) { - case 'always': - sendToChannel = true; - break; - case 'never': - sendToChannel = false; - break; - default: - sendToChannel = !this.data.mainMessage?.tcount ?? true; - } - - const { mainMessage } = Template.currentData() as ThreadTemplateInstance['data']; - - this.state = new ReactiveDict(undefined, { - sendToChannel, - tmid: mainMessage?._id, - rid: mainMessage?.rid, - }); - - this.loadMore = async () => { - const { tmid } = Tracker.nonreactive(() => this.state.all()); - if (!tmid) { - return; - } - - this.state.set('loading', true); - - const messages = await callWithErrorHandling('getThreadMessages', { tmid }); - - upsertMessageBulk({ msgs: messages }, this.Threads); - upsertMessageBulk({ msgs: messages }, Messages); - - Tracker.afterFlush(() => { - this.state.set('loading', false); - }); - }; - - this.closeThread = () => { - const { - route, - params: { context, tab, ...params }, - } = FlowRouter.current(); - if (!route || !route.name) { - throw new Error('FlowRouter.current().route.name is undefined'); - } - FlowRouter.go(route.name, params); - }; -}); - -Template.thread.onRendered(function (this: ThreadTemplateInstance) { - const rid = Tracker.nonreactive(() => this.state.get('rid')); - if (!rid) { - throw new Error('No rid found'); - } - - this.atBottom = true; - this.wrapper = this.find('.js-scroll-thread'); - - this.sendToBottom = withThrottling({ wait: 300 })(() => { - this.atBottom = true; - if (this.wrapper) { - this.wrapper.scrollTop = this.wrapper.scrollHeight; - } - }); - - this.sendToBottomIfNecessary = () => { - this.atBottom && this.sendToBottom(); - }; - - const list = this.firstNode.querySelector('.js-scroll-thread ul'); - - if (!list) { - throw new Error('Could not find list element'); - } - - const observer = new ResizeObserver(this.sendToBottomIfNecessary); - observer.observe(list); - - this.autorun(() => { - const rid = this.state.get('rid'); - const tmid = this.state.get('tmid'); - if (!rid) { - return; - } - this.callbackRemove?.(); - - this.callbackRemove = () => callbacks.remove('streamNewMessage', `thread-${rid}`); - - callbacks.add( - 'streamNewMessage', - withDebouncing({ wait: 1000 })((msg: IEditedMessage) => { - if (Session.get('openedRoom') !== msg.rid || rid !== msg.rid || msg.editedAt || msg.tmid !== tmid) { - return; - } - Meteor.call('readThreads', tmid); - }), - callbacks.priority.MEDIUM, - `thread-${rid}`, - ); - }); - - this.autorun(() => { - const tmid = this.state.get('tmid'); - this.threadsObserve?.stop(); - - this.threadsObserve = Messages.find( - { $or: [{ tmid }, { _id: tmid }], _hidden: { $ne: true } }, - { - fields: { - collapsed: 0, - threadMsg: 0, - repliesCount: 0, - }, - }, - ).observe({ - added: ({ _id, ...message }: IMessage) => { - this.Threads.upsert({ _id }, message); - }, - changed: ({ _id, ...message }: IMessage) => { - this.Threads.update({ _id }, message); - }, - removed: ({ _id }: IMessage) => this.Threads.remove(_id), - }); - - this.loadMore(); - }); - - this.autorun(() => { - FlowRouter.watchPathChange(); - const jump = FlowRouter.getQueryParam('jump'); - const { mainMessage } = Template.currentData() as ThreadTemplateInstance['data']; - this.state.set({ - tmid: mainMessage?._id, - rid: mainMessage?.rid, - jump, - }); - }); - - this.autorun(() => { - const jump = this.state.get('jump'); - const loading = this.state.get('loading'); - - if (jump && this.lastJump !== jump && loading === false) { - this.lastJump = jump; - this.find('.js-scroll-thread').style.scrollBehavior = 'smooth'; - this.state.set('jump', null); - Tracker.afterFlush(() => { - const message = this.find(`#thread-${jump}`); - message.classList.add('highlight'); - const removeClass = () => { - message.classList.remove('highlight'); - message.removeEventListener('animationend', removeClass); - }; - message.addEventListener('animationend', removeClass); - setTimeout(() => { - message.scrollIntoView(); - }, 300); - }); - } - }); - - this.autorun(() => { - const { Threads, state } = Template.instance() as ThreadTemplateInstance; - const tmid = state.get('tmid'); - const threads = Threads.findOne({ $or: [{ tmid }, { _id: tmid }] }); - const isLoading = state.get('loading'); - - if (!isLoading && !threads) { - this.closeThread(); - } - }); - - this.autorun(() => { - const { messageHighlightContext } = Template.currentData() as ThreadTemplateInstance['data']; - - this.state.set('editingMID', messageHighlightContext.highlightMessageId); - }); -}); - -Template.thread.onDestroyed(function (this: ThreadTemplateInstance) { - const { Threads, threadsObserve, callbackRemove } = this; - Threads.remove({}); - threadsObserve?.stop(); - - callbackRemove?.(); -}); diff --git a/apps/meteor/app/threads/client/flextab/threads.ts b/apps/meteor/app/threads/client/flextab/threads.ts deleted file mode 100644 index 19a5c0fc47b9e..0000000000000 --- a/apps/meteor/app/threads/client/flextab/threads.ts +++ /dev/null @@ -1 +0,0 @@ -import '../threads.css'; diff --git a/apps/meteor/app/threads/client/index.ts b/apps/meteor/app/threads/client/index.ts index c984a3eaaba15..4dad7ebd4846c 100644 --- a/apps/meteor/app/threads/client/index.ts +++ b/apps/meteor/app/threads/client/index.ts @@ -1,6 +1,5 @@ import './flextab/threadlist'; -import './flextab/thread.ts'; -import './flextab/threads.ts'; import './messageAction/follow'; import './messageAction/unfollow'; import './messageAction/replyInThread'; +import './threads.css'; diff --git a/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx b/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx index 6831f2df79012..5756821cc7d36 100644 --- a/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx +++ b/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx @@ -5,16 +5,16 @@ import { Template } from 'meteor/templating'; import type { ReactElement } from 'react'; import React, { memo, useCallback, useRef } from 'react'; -import { ChatMessage } from '../../../../../app/models/client'; +import { Messages } from '../../../../../app/models/client'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; -import { useMessageContext } from './useMessageContext'; +import { useRoomMessageContext } from './useRoomMessageContext'; type LegacyMessageTemplateListProps = { room: IRoom; }; const LegacyMessageTemplateList = ({ room }: LegacyMessageTemplateListProps): ReactElement => { - const messageContext = useMessageContext(room); + const roomMessageContext = useRoomMessageContext(room); const hideSystemMessages = useSetting('Hide_System_Messages') as MessageTypesValues[]; @@ -43,7 +43,7 @@ const LegacyMessageTemplateList = ({ room }: LegacyMessageTemplateListProps): Re }, }; - return ChatMessage.find(query, options).fetch(); + return Messages.find(query, options).fetch(); }, [hideSystemMessages, room._id, room.sysMes]), ); @@ -59,7 +59,7 @@ const LegacyMessageTemplateList = ({ room }: LegacyMessageTemplateListProps): Re index, shouldCollapseReplies: false, msg: message, - ...messageContext, + ...roomMessageContext, }), node.parentElement, node, @@ -76,7 +76,7 @@ const LegacyMessageTemplateList = ({ room }: LegacyMessageTemplateListProps): Re viewsRef.current.delete(message._id); } }, - [messageContext], + [roomMessageContext], ); return ( diff --git a/apps/meteor/client/views/room/components/body/useMessageContext.ts b/apps/meteor/client/views/room/components/body/useRoomMessageContext.ts similarity index 98% rename from apps/meteor/client/views/room/components/body/useMessageContext.ts rename to apps/meteor/client/views/room/components/body/useRoomMessageContext.ts index 63cd5ccf8f63a..04867f43b8d66 100644 --- a/apps/meteor/client/views/room/components/body/useMessageContext.ts +++ b/apps/meteor/client/views/room/components/body/useRoomMessageContext.ts @@ -8,7 +8,7 @@ import { useReactiveValue } from '../../../../hooks/useReactiveValue'; import { useRoomSubscription } from '../../contexts/RoomContext'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const useMessageContext = (room: IRoom) => { +export const useRoomMessageContext = (room: IRoom) => { const uid = useUserId(); const user = useUser() ?? undefined; const rid = room._id; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx index d29278e46cdbb..8bf01405c52d9 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -1,20 +1,29 @@ -import type { IThreadMainMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; +import { isThreadMessage, isEditedMessage } from '@rocket.chat/core-typings'; import { CheckBox } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useTranslation, useUserPreference } from '@rocket.chat/ui-contexts'; -import type { ReactElement } from 'react'; -import React, { useState, useEffect, useCallback, useContext, useRef } from 'react'; +import { useCurrentRoute, useMethod, useQueryStringParameter, useRoute, useTranslation, useUserPreference } from '@rocket.chat/ui-contexts'; +import type { ReactElement, UIEvent } from 'react'; +import React, { useState, useEffect, useCallback, useContext, useRef, useMemo } from 'react'; -import type { ThreadTemplateInstance } from '../../../../../../app/threads/client/flextab/thread'; +import { Messages } from '../../../../../../app/models/client'; +import { upsertMessageBulk } from '../../../../../../app/ui-utils/client/lib/RoomHistoryManager'; +import type { CommonRoomTemplateInstance } from '../../../../../../app/ui/client/views/app/lib/CommonRoomTemplateInstance'; +import { getCommonRoomEvents } from '../../../../../../app/ui/client/views/app/lib/getCommonRoomEvents'; +import { callbacks } from '../../../../../../lib/callbacks'; +import { isTruthy } from '../../../../../../lib/isTruthy'; import VerticalBar from '../../../../../components/VerticalBar'; +import { useReactiveValue } from '../../../../../hooks/useReactiveValue'; import MessageHighlightContext from '../../../MessageList/contexts/MessageHighlightContext'; import DropTargetOverlay from '../../../components/body/DropTargetOverlay'; +import LoadingMessagesIndicator from '../../../components/body/LoadingMessagesIndicator'; import ComposerContainer from '../../../components/body/composer/ComposerContainer'; import { useFileUploadDropTarget } from '../../../components/body/useFileUploadDropTarget'; +import { useRoomMessageContext } from '../../../components/body/useRoomMessageContext'; import { useChat } from '../../../contexts/ChatContext'; import { MessageContext } from '../../../contexts/MessageContext'; import { useRoom, useRoomSubscription } from '../../../contexts/RoomContext'; -import { useTabBarClose, useTabBarOpenUserInfo } from '../../../contexts/ToolboxContext'; +import { useTabBarClose, useToolboxContext } from '../../../contexts/ToolboxContext'; type ThreadChatProps = { mainMessage: IThreadMainMessage; @@ -22,7 +31,8 @@ type ThreadChatProps = { const ThreadChat = ({ mainMessage }: ThreadChatProps): ReactElement => { const t = useTranslation(); - const ref = useRef(null); + const atBottomRef = useRef(true); + const wrapperRef = useRef(null); const messageContext = useContext(MessageContext); @@ -34,13 +44,11 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps): ReactElement => { const messageHighlightContext = useContext(MessageHighlightContext); - const room = useRoom(); const subscription = useRoomSubscription(); - const openRoomInfo = useTabBarOpenUserInfo(); const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(); - const sendToChannelPreference = useUserPreference('alsoSendThreadToChannel'); + const sendToChannelPreference = useUserPreference<'always' | 'never' | 'default'>('alsoSendThreadToChannel'); const [sendToChannel, setSendToChannel] = useState(() => { switch (sendToChannelPreference) { @@ -52,6 +60,9 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps): ReactElement => { return !mainMessage.tcount; } }); + const [loading, setLoading] = useState(false); + + const hideUsernames = useUserPreference('hideUsernames'); const handleSend = useCallback((): void => { if (sendToChannelPreference === 'default') { @@ -59,46 +70,65 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps): ReactElement => { } }, [sendToChannelPreference]); - const reactiveViewDataRef = useRef( - new ReactiveVar({ - mainMessage, - subscription, - rid: room._id, - tabBar: { openRoomInfo }, - chatContext: chat, - messageContext, - messageHighlightContext, - sendToChannel, - onSend: handleSend, + const sendToBottom = useCallback(() => { + const wrapper = wrapperRef.current; + + wrapper?.scrollTo(30, wrapper.scrollHeight); + }, []); + + const sendToBottomIfNecessary = useCallback(() => { + if (atBottomRef.current === true) { + sendToBottom(); + } + }, [sendToBottom]); + + const room = useRoom(); + const roomMessageContext = useRoomMessageContext(room); + const threadMessageContext = useMemo( + () => ({ + ...roomMessageContext, + settings: { + ...roomMessageContext.settings, + showReplyButton: false, + showreply: false, + }, }), + [roomMessageContext], ); - useEffect(() => { - reactiveViewDataRef.current.set({ - mainMessage, - subscription, - rid: room._id, - tabBar: { openRoomInfo }, - chatContext: chat, - messageContext, - messageHighlightContext, - sendToChannel, - onSend: handleSend, - }); - }, [chat, handleSend, mainMessage, messageContext, messageHighlightContext, openRoomInfo, room._id, sendToChannel, subscription]); - - const viewDataFn = useCallback(() => reactiveViewDataRef.current.get(), []); + const customClassMain = useMemo(() => { + return ['thread-main', mainMessage._id === messageHighlightContext.highlightMessageId ? 'editing' : ''].filter(Boolean).join(' '); + }, [mainMessage._id, messageHighlightContext.highlightMessageId]); - useEffect(() => { - if (!ref.current) { - return; - } - const view = Blaze.renderWithData(Template.thread, viewDataFn, ref.current); + const customClass = useCallback( + (message: IMessage): string => { + return message._id === messageHighlightContext.highlightMessageId ? 'editing' : ''; + }, + [messageHighlightContext.highlightMessageId], + ); - return (): void => { - Blaze.remove(view); - }; - }, [viewDataFn]); + const messages = useReactiveValue( + useCallback(() => { + return Messages.find( + { + $or: [{ tmid: mainMessage._id }, { _id: mainMessage._id }], + _hidden: { $ne: true }, + tmid: mainMessage._id, + _id: { $ne: mainMessage._id }, + }, + { + fields: { + collapsed: 0, + threadMsg: 0, + repliesCount: 0, + }, + sort: { ts: 1 }, + }, + ) + .fetch() + .filter(isThreadMessage); + }, [mainMessage._id]), + ); const sendToChannelID = useUniqueId(); @@ -122,34 +152,276 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps): ReactElement => { [chat], ); + const handleScroll = useCallback(({ currentTarget: e }: UIEvent) => { + atBottomRef.current = e.scrollTop >= e.scrollHeight - e.clientHeight; + }, []); + + const useLegacyMessageTemplate = useUserPreference('useLegacyMessageTemplate') ?? false; + const toolbox = useToolboxContext(); + + useEffect(() => { + const messageList = wrapperRef.current?.querySelector('ul'); + + if (!messageList) { + return; + } + + const messageEvents: Record void> = { + ...getCommonRoomEvents(useLegacyMessageTemplate), + 'click .toggle-hidden'(event: JQuery.ClickEvent) { + const mid = event.target.dataset.message; + if (mid) document.getElementById(mid)?.classList.toggle('message--ignored'); + }, + 'load .gallery-item'() { + sendToBottomIfNecessary(); + }, + 'rendered .js-block-wrapper'() { + sendToBottomIfNecessary(); + }, + }; + + const eventHandlers = Object.entries(messageEvents).map(([key, handler]) => { + const [, event, selector] = key.match(/^(.+?)\s(.+)$/) ?? [key, key]; + return { + event, + selector, + listener: (e: JQuery.TriggeredEvent) => + handler.call(null, e, { data: { rid: room._id, tabBar: toolbox, chatContext: chat } }), + }; + }); + + for (const { event, selector, listener } of eventHandlers) { + $(messageList).on(event, selector, listener); + } + + return () => { + for (const { event, selector, listener } of eventHandlers) { + $(messageList).off(event, selector, listener); + } + }; + }, [chat, room._id, sendToBottomIfNecessary, toolbox, useLegacyMessageTemplate]); + + useEffect(() => { + const messageList = wrapperRef.current?.querySelector('ul'); + + if (!messageList) { + return; + } + + const observer = new ResizeObserver(() => { + sendToBottomIfNecessary(); + }); + + observer.observe(messageList); + + return () => { + observer?.disconnect(); + }; + }, [sendToBottomIfNecessary]); + + const handleComposerResize = useCallback((): void => { + sendToBottomIfNecessary(); + }, [sendToBottomIfNecessary]); + + const readThreads = useMethod('readThreads'); + + useEffect(() => { + callbacks.add( + 'streamNewMessage', + (msg: IMessage) => { + if (room._id !== msg.rid || (isEditedMessage(msg) && msg.editedAt) || msg.tmid !== mainMessage._id) { + return; + } + + readThreads(mainMessage._id); + }, + callbacks.priority.MEDIUM, + `thread-${room._id}`, + ); + + return () => { + callbacks.remove('streamNewMessage', `thread-${room._id}`); + }; + }, [mainMessage._id, readThreads, room._id, sendToBottom]); + + const jump = useQueryStringParameter('jump'); + const [currentRouteName, currentRouteParams, currentRouteQueryStringParams] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No route name'); + } + const currentRoute = useRoute(currentRouteName); + + useEffect(() => { + if (loading || !jump) { + return; + } + + const newQueryStringParams = { ...currentRouteQueryStringParams }; + delete newQueryStringParams.jump; + currentRoute.replace(currentRouteParams, newQueryStringParams); + + const messageElement = document.querySelector(`#thread-${jump}`); + if (!messageElement) { + return; + } + + messageElement.classList.add('highlight'); + const removeClass = () => { + messageElement.classList.remove('highlight'); + messageElement.removeEventListener('animationend', removeClass); + }; + messageElement.addEventListener('animationend', removeClass); + setTimeout(() => { + messageElement.scrollIntoView(); + }, 300); + }, [currentRoute, currentRouteParams, currentRouteQueryStringParams, jump, loading]); + + const getThreadMessages = useMethod('getThreadMessages'); + + useEffect(() => { + setLoading(true); + getThreadMessages({ tmid: mainMessage._id }).then((messages) => { + upsertMessageBulk({ msgs: messages }, Messages); + setLoading(false); + }); + }, [getThreadMessages, mainMessage._id]); + + const viewsRef = useRef>(new Map()); + + const mainMessageRef = useCallback( + (mainMessage: IMessage, index: number) => (node: HTMLLIElement | null) => { + if (node?.parentElement) { + const view = Blaze.renderWithData( + Template.message, + () => ({ + index, + groupable: false, + hideRoles: true, + msg: mainMessage, + room: threadMessageContext.room, + subscription: threadMessageContext.subscription, + settings: threadMessageContext.settings, + templatePrefix: 'thread-', + customClass: customClassMain, + u: threadMessageContext.u, + ignored: false, + shouldCollapseReplies: true, + chatContext: chat, + messageContext, + }), + node.parentElement, + node, + ); + + viewsRef.current.set(mainMessage._id, view); + } + + if (!node && viewsRef.current.has(mainMessage._id)) { + const view = viewsRef.current.get(mainMessage._id); + if (view) { + Blaze.remove(view); + } + viewsRef.current.delete(mainMessage._id); + } + }, + [ + chat, + customClassMain, + messageContext, + threadMessageContext.room, + threadMessageContext.settings, + threadMessageContext.subscription, + threadMessageContext.u, + ], + ); + + const messageRef = useCallback( + (message: IMessage, index: number) => (node: HTMLLIElement | null) => { + if (node?.parentElement) { + const view = Blaze.renderWithData( + Template.message, + () => ({ + index, + hideRoles: true, + msg: message, + room: threadMessageContext.room, + shouldCollapseReplies: true, + subscription: threadMessageContext.subscription, + settings: threadMessageContext.settings, + templatePrefix: 'thread-', + customClass: customClass(message), + u: threadMessageContext.u, + context: 'threads', + chatContext: chat, + messageContext, + }), + node.parentElement, + node, + ); + + viewsRef.current.set(message._id, view); + } + + if (!node && viewsRef.current.has(message._id)) { + const view = viewsRef.current.get(message._id); + if (view) { + Blaze.remove(view); + } + viewsRef.current.delete(message._id); + } + }, + [ + chat, + customClass, + messageContext, + threadMessageContext.room, + threadMessageContext.settings, + threadMessageContext.subscription, + threadMessageContext.u, + ], + ); + return ( - <> + - - +
+
+
    + {loading ? ( +
  • + +
  • + ) : ( + <> +
  • + {messages.map((message, index) => ( +
  • + ))} + + )} +
+
-
-
-
- setSendToChannel((checked) => !checked)} /> -
- -
-
- - + +
+
); }; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts index 253b16f235274..173fac9682155 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts @@ -46,7 +46,7 @@ export class HomeFlextab { get flexTabViewThreadMessage(): Locator { return this.page.locator( - 'div.thread-list.js-scroll-thread ul.thread [data-qa-type="message"]:last-child div.message-body-wrapper [data-qa-type="message-body"]', + 'div.thread-list ul.thread [data-qa-type="message"]:last-child div.message-body-wrapper [data-qa-type="message-body"]', ); } diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index c3fba2b357824..d94f6cd0ed803 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -27,6 +27,9 @@ "react": "~17.0.2", "use-sync-external-store": "^1.2.0" }, + "volta": { + "extends": "../../package.json" + }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts index ebb8a12b17c54..17998d5181077 100644 --- a/packages/ui-contexts/src/ServerContext/methods.ts +++ b/packages/ui-contexts/src/ServerContext/methods.ts @@ -183,6 +183,7 @@ export interface ServerMethods { 'personalAccessTokens:removeToken': (...args: any[]) => any; 'e2e.requestSubscriptionKeys': (...args: any[]) => any; 'readMessages': (...args: any[]) => any; + 'readThreads': (tmid: IMessage['_id']) => void; 'refreshClients': (...args: any[]) => any; 'refreshOAuthService': (...args: any[]) => any; 'registerUser': (...args: any[]) => any;