+
{{> sidebarFooter}}
diff --git a/apps/meteor/app/ui-utils/client/lib/readMessages.ts b/apps/meteor/app/ui-utils/client/lib/readMessages.ts
index d36a76d90866..b6a90fcf4758 100644
--- a/apps/meteor/app/ui-utils/client/lib/readMessages.ts
+++ b/apps/meteor/app/ui-utils/client/lib/readMessages.ts
@@ -160,7 +160,7 @@ export class ReadMessage extends Emitter {
if (firstUnreadRecord) {
room.unreadFirstId = firstUnreadRecord._id;
document.querySelector('.message.first-unread')?.classList.remove('first-unread');
- document.querySelector(`.message#${firstUnreadRecord._id}`)?.classList.add('first-unread');
+ document.querySelector(`.message[data-id="${firstUnreadRecord._id}"]`)?.classList.add('first-unread');
}
}
}
diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts
index ab65439e2352..4c2e1053a4fc 100644
--- a/apps/meteor/app/ui/client/lib/ChatMessages.ts
+++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts
@@ -43,8 +43,7 @@ export class ChatMessages implements ChatAPI {
return;
}
- this.composer.setText((await this.data.getDraft(undefined)) ?? '');
- await this.currentEditing.stop();
+ await this.currentEditing.cancel();
},
toNextMessage: async () => {
if (!this.composer || !this.currentEditing) {
@@ -55,18 +54,17 @@ export class ChatMessages implements ChatAPI {
const nextMessage = currentMessage ? await this.data.findNextOwnMessage(currentMessage) : undefined;
if (nextMessage) {
- this.messageEditing.editMessage(nextMessage, { cursorAtStart: true });
+ await this.messageEditing.editMessage(nextMessage, { cursorAtStart: true });
return;
}
- await this.currentEditing.stop();
- this.composer.setText((await this.data.getDraft(undefined)) ?? '');
+ await this.currentEditing.cancel();
},
editMessage: async (message: IMessage, { cursorAtStart = false }: { cursorAtStart?: boolean } = {}) => {
const text = (await this.data.getDraft(message._id)) || message.attachments?.[0].description || message.msg;
const cursorPosition = cursorAtStart ? 0 : text.length;
- this.currentEditing?.stop();
+ await this.currentEditing?.stop();
if (!this.composer || !(await this.data.canUpdateMessage(message))) {
return;
@@ -152,16 +150,17 @@ export class ChatMessages implements ChatAPI {
}
await this.data.discardDraft(this.currentEditingMID);
- this.currentEditing?.stop();
+ await this.currentEditing?.stop();
+ this.composer?.setText((await this.data.getDraft(undefined)) ?? '');
},
};
}
- private release() {
+ private async release() {
this.composer?.release();
if (this.currentEditing) {
if (!this.params.tmid) {
- this.currentEditing.cancel();
+ await this.currentEditing.cancel();
}
this.composer?.clear();
}
diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx
index 191f72c8d690..a5591227aad9 100644
--- a/apps/meteor/client/components/MarkdownText.tsx
+++ b/apps/meteor/client/components/MarkdownText.tsx
@@ -26,7 +26,7 @@ marked.Lexer.rules.gfm = {
};
const linkMarked = (href: string | null, _title: string | null, text: string): string =>
- `
${text} `;
+ `
${text} `;
const paragraphMarked = (text: string): string => text;
const brMarked = (): string => ' ';
const listItemMarked = (text: string): string => {
@@ -46,6 +46,9 @@ inlineRenderer.hr = horizontalRuleMarked;
inlineWithoutBreaks.link = linkMarked;
inlineWithoutBreaks.paragraph = paragraphMarked;
inlineWithoutBreaks.br = brMarked;
+inlineWithoutBreaks.image = brMarked;
+inlineWithoutBreaks.code = paragraphMarked;
+inlineWithoutBreaks.codespan = paragraphMarked;
inlineWithoutBreaks.listitem = listItemMarked;
inlineWithoutBreaks.hr = horizontalRuleMarked;
@@ -118,6 +121,15 @@ const MarkdownText: FC
> = ({
}
})();
+ // Add a hook to make all links open a new window
+ dompurify.addHook('afterSanitizeAttributes', (node) => {
+ // set all elements owning target to target=_blank
+ if ('target' in node) {
+ node.setAttribute('target', '_blank');
+ node.setAttribute('rel', 'nofollow noopener noreferrer');
+ }
+ });
+
return preserveHtml ? html : html && sanitizer(html, { ADD_ATTR: ['target'], ALLOWED_URI_REGEXP: getRegexp(schemes) });
}, [preserveHtml, sanitizer, content, variant, markedOptions, parseEmoji, schemes]);
diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx
index 416cba913386..aed5c44f39ed 100644
--- a/apps/meteor/client/components/Page/PageHeader.tsx
+++ b/apps/meteor/client/components/Page/PageHeader.tsx
@@ -19,11 +19,15 @@ const PageHeader: FC = ({ children = undefined, title, onClickB
const { isMobile } = useLayout();
return (
-
+
= ({ title, onClose, children, ...props }) => (
{(title || onClose) && (
{title && (
-
+
{title}
)}
diff --git a/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx b/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx
index feeb1176632e..7fc522c31f09 100644
--- a/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx
+++ b/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx
@@ -1,5 +1,4 @@
-import { css } from '@rocket.chat/css-in-js';
-import { Box } from '@rocket.chat/fuselage';
+import { Box, SidebarItem } from '@rocket.chat/fuselage';
import type colors from '@rocket.chat/fuselage-tokens/colors';
import type { ReactElement, ReactNode } from 'react';
import React, { memo } from 'react';
@@ -7,6 +6,7 @@ import React, { memo } from 'react';
type SidebarGenericItemProps = {
href?: string;
active?: boolean;
+ featured?: boolean;
children: ReactNode;
customColors?: {
default: typeof colors[string];
@@ -16,42 +16,12 @@ type SidebarGenericItemProps = {
textColor?: string;
};
-const SidebarGenericItem = ({
- href,
- active,
- children,
- customColors,
- textColor = 'default',
- ...props
-}: SidebarGenericItemProps): ReactElement => (
-
-
+const SidebarGenericItem = ({ href, active, children, ...props }: SidebarGenericItemProps): ReactElement => (
+
+
{children}
-
+
);
export default memo(SidebarGenericItem);
diff --git a/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx b/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx
index 24c386bb32b2..131be118e30d 100644
--- a/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx
+++ b/apps/meteor/client/components/Sidebar/SidebarNavigationItem.tsx
@@ -35,8 +35,8 @@ const SidebarNavigationItem: FC = ({
return (
{icon && }
-
- {label} {tag && {tag}}
+
+ {label} {tag && {tag}}
);
diff --git a/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx b/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx
index 5a186cd8e830..afe5b5b15b40 100644
--- a/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx
+++ b/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx
@@ -3,13 +3,15 @@ import { IconButton } from '@rocket.chat/fuselage';
import type { ReactElement, MouseEventHandler, ComponentProps } from 'react';
import React, { memo } from 'react';
-const VerticalBarAction = ({
- name,
- ...props
-}: {
+type VerticalBarActionProps = {
name: ComponentProps['name'];
title?: string;
+ disabled?: boolean;
onClick?: MouseEventHandler;
-}): ReactElement => ;
+};
+
+const VerticalBarAction = ({ name, ...props }: VerticalBarActionProps): ReactElement => (
+
+);
export default memo(VerticalBarAction);
diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts
index 970e2a58d2d2..982092d56ac5 100644
--- a/apps/meteor/client/lib/chats/ChatAPI.ts
+++ b/apps/meteor/client/lib/chats/ChatAPI.ts
@@ -87,7 +87,7 @@ export type ChatAPI = {
| undefined;
readonly flows: {
readonly uploadFiles: (files: readonly File[]) => Promise;
- readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean }) => Promise;
+ readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean }) => Promise;
readonly processSlashCommand: (message: IMessage) => Promise;
readonly processTooLongMessage: (message: IMessage) => Promise;
readonly processMessageEditing: (message: Pick & Partial>) => Promise;
diff --git a/apps/meteor/client/lib/chats/data.ts b/apps/meteor/client/lib/chats/data.ts
index f2cfa17528d0..b0e323cd6ffb 100644
--- a/apps/meteor/client/lib/chats/data.ts
+++ b/apps/meteor/client/lib/chats/data.ts
@@ -12,7 +12,6 @@ import { call } from '../utils/call';
import { prependReplies } from '../utils/prependReplies';
import type { DataAPI } from './ChatAPI';
-const messagesCollection = Messages as Mongo.Collection;
const roomsCollection = Rooms as Mongo.Collection;
const subscriptionsCollection = Subscriptions as Mongo.Collection;
@@ -38,7 +37,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
};
const findMessageByID = async (mid: IMessage['_id']): Promise =>
- messagesCollection.findOne({ _id: mid, _hidden: { $ne: true } }, { reactive: false }) ?? call('getSingleMessage', mid);
+ Messages.findOne({ _id: mid, _hidden: { $ne: true } }, { reactive: false }) ?? call('getSingleMessage', mid);
const getMessageByID = async (mid: IMessage['_id']): Promise => {
const message = await findMessageByID(mid);
@@ -51,7 +50,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
};
const findLastMessage = async (): Promise =>
- messagesCollection.findOne({ rid, tmid: tmid ?? { $exists: false }, _hidden: { $ne: true } }, { sort: { ts: -1 }, reactive: false });
+ Messages.findOne({ rid, tmid: tmid ?? { $exists: false }, _hidden: { $ne: true } }, { sort: { ts: -1 }, reactive: false });
const getLastMessage = async (): Promise => {
const message = await findLastMessage();
@@ -70,7 +69,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
return undefined;
}
- return messagesCollection.findOne(
+ return Messages.findOne(
{ rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true } },
{ sort: { ts: -1 }, reactive: false },
);
@@ -93,7 +92,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
return undefined;
}
- return messagesCollection.findOne(
+ return Messages.findOne(
{ rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true }, 'ts': { $lt: message.ts } },
{ sort: { ts: -1 }, reactive: false },
);
@@ -116,7 +115,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
return undefined;
}
- return messagesCollection.findOne(
+ return Messages.findOne(
{ rid, 'tmid': tmid ?? { $exists: false }, 'u._id': uid, '_hidden': { $ne: true }, 'ts': { $gt: message.ts } },
{ sort: { ts: 1 }, reactive: false },
);
@@ -133,7 +132,7 @@ export const createDataAPI = ({ rid, tmid }: { rid: IRoom['_id']; tmid: IMessage
};
const pushEphemeralMessage = async (message: Omit): Promise => {
- messagesCollection.upsert({ _id: message._id }, { $set: { ...message, rid, ...(tmid && { tmid }) } });
+ Messages.upsert({ _id: message._id }, { $set: { ...message, rid, ...(tmid && { tmid }) } });
};
const canUpdateMessage = async (message: IMessage): Promise => {
diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts
index 9320e7fcf05b..e40394d515d2 100644
--- a/apps/meteor/client/lib/chats/flows/sendMessage.ts
+++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts
@@ -32,13 +32,13 @@ const process = async (chat: ChatAPI, message: IMessage): Promise => {
await call('sendMessage', message);
};
-export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string; tshow?: boolean }): Promise => {
+export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string; tshow?: boolean }): Promise => {
if (!(await chat.data.isSubscribedToRoom())) {
try {
await chat.data.joinRoom();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
- return;
+ return false;
}
}
@@ -48,7 +48,7 @@ export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string
if (!text && !chat.currentEditing) {
// Nothing to do
- return;
+ return false;
}
if (text) {
@@ -64,7 +64,7 @@ export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
- return;
+ return true;
}
if (chat.currentEditing) {
@@ -72,20 +72,22 @@ export const sendMessage = async (chat: ChatAPI, { text, tshow }: { text: string
if (!originalMessage) {
dispatchToastMessage({ type: 'warning', message: t('Message_not_found') });
- return;
+ return false;
}
try {
if (await chat.flows.processMessageEditing({ ...originalMessage, msg: '' })) {
chat.currentEditing.stop();
- return;
+ return false;
}
await chat.currentEditing?.reset();
await chat.flows.requestMessageDeletion(originalMessage);
- return;
+ return false;
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}
+
+ return false;
};
diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx
index e4bd8c946e20..e6896093a1d5 100644
--- a/apps/meteor/client/providers/MeteorProvider.tsx
+++ b/apps/meteor/client/providers/MeteorProvider.tsx
@@ -25,11 +25,11 @@ const MeteorProvider: FC = ({ children }) => (
-
-
-
-
-
+
+
+
+
+
@@ -51,11 +51,11 @@ const MeteorProvider: FC = ({ children }) => (
-
-
-
-
-
+
+
+
+
+
diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx
index 711779ae52cf..8272f6acd7bb 100644
--- a/apps/meteor/client/providers/TranslationProvider.tsx
+++ b/apps/meteor/client/providers/TranslationProvider.tsx
@@ -1,5 +1,6 @@
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
-import { TranslationContext, useAbsoluteUrl } from '@rocket.chat/ui-contexts';
+import { useSetting, TranslationContext, useAbsoluteUrl } from '@rocket.chat/ui-contexts';
import i18next from 'i18next';
import I18NextHttpBackend from 'i18next-http-backend';
import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n';
@@ -16,46 +17,105 @@ type TranslationNamespace = Extract exten
: never
: never;
-const namespaces = ['onboarding', 'registration'] as TranslationNamespace[];
+const namespacesDefault = ['onboarding', 'registration'] as TranslationNamespace[];
+
+const parseToJSON = (customTranslations: string) => {
+ try {
+ return JSON.parse(customTranslations);
+ } catch (e) {
+ return false;
+ }
+};
const useI18next = (lng: string): typeof i18next => {
const basePath = useAbsoluteUrl()('/i18n');
- const i18n = useState(() => {
+ const customTranslations = useSetting('Custom_Translations');
+
+ const parse = useMutableCallback((data: string, lngs?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => {
+ const parsedCustomTranslations = typeof customTranslations === 'string' && parseToJSON(customTranslations);
+
+ const source = JSON.parse(data);
+ const result: { [key: string]: any } = {};
+
+ for (const [key, value] of Object.entries(source)) {
+ const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`));
+
+ if (prefix) {
+ result[key.slice(prefix.length + 1)] = value;
+ }
+ }
+
+ if (lngs && parsedCustomTranslations) {
+ for (const language of Array.isArray(lngs) ? lngs : [lngs]) {
+ if (!parsedCustomTranslations[language]) {
+ continue;
+ }
+
+ for (const [key, value] of Object.entries(parsedCustomTranslations[language])) {
+ const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`));
+
+ if (prefix) {
+ result[key.slice(prefix.length + 1)] = value;
+ }
+ }
+ }
+ }
+
+ return result;
+ });
+
+ const [i18n] = useState(() => {
const i18n = i18next.createInstance().use(I18NextHttpBackend).use(initReactI18next);
i18n.init({
lng,
fallbackLng: 'en',
- ns: namespaces,
+ ns: namespacesDefault,
nsSeparator: '.',
+ partialBundledLanguages: true,
debug: false,
backend: {
loadPath: `${basePath}/{{lng}}.json`,
- parse: (data: string, _languages?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => {
- const source = JSON.parse(data);
- const result: { [key: string]: any } = {};
-
- for (const key of Object.keys(source)) {
- const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`));
-
- if (prefix) {
- result[key.slice(prefix.length + 1)] = source[key];
- }
- }
-
- return result;
- },
+ parse,
},
});
return i18n;
- })[0];
+ });
useEffect(() => {
i18n.changeLanguage(lng);
}, [i18n, lng]);
+ useEffect(() => {
+ if (!customTranslations || typeof customTranslations !== 'string') {
+ return;
+ }
+
+ const parsedCustomTranslations: Record> = JSON.parse(customTranslations);
+
+ for (const [ln, translations] of Object.entries(parsedCustomTranslations)) {
+ const namespaces = Object.entries(translations).reduce((acc, [key, value]): Record> => {
+ const namespace = key.split('.')[0];
+
+ if (namespacesDefault.includes(namespace as unknown as TranslationNamespace)) {
+ acc[namespace] = acc[namespace] ?? {};
+ acc[namespace][key] = value;
+ acc[namespace][key.slice(namespace.length + 1)] = value;
+ return acc;
+ }
+ acc.project = acc.project ?? {};
+ acc.project[key] = value;
+ return acc;
+ }, {} as Record>);
+
+ for (const [namespace, translations] of Object.entries(namespaces)) {
+ i18n.addResourceBundle(ln, namespace, translations);
+ }
+ }
+ }, [customTranslations, i18n]);
+
return i18n;
};
diff --git a/apps/meteor/client/sidebar/RoomList/RoomList.tsx b/apps/meteor/client/sidebar/RoomList/RoomList.tsx
index 09ad31e2b50e..f74729b23b11 100644
--- a/apps/meteor/client/sidebar/RoomList/RoomList.tsx
+++ b/apps/meteor/client/sidebar/RoomList/RoomList.tsx
@@ -10,7 +10,6 @@ import { useAvatarTemplate } from '../hooks/useAvatarTemplate';
import { usePreventDefault } from '../hooks/usePreventDefault';
import { useRoomList } from '../hooks/useRoomList';
import { useShortcutOpenMenu } from '../hooks/useShortcutOpenMenu';
-import { useSidebarPaletteColor } from '../hooks/useSidebarPaletteColor';
import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode';
import RoomListRow from './RoomListRow';
import ScrollerWithCustomProps from './ScrollerWithCustomProps';
@@ -43,7 +42,6 @@ const RoomList = (): ReactElement => {
usePreventDefault(ref);
useShortcutOpenMenu(ref);
- useSidebarPaletteColor();
return (
diff --git a/apps/meteor/client/sidebar/RoomList/normalizeSidebarMessage.ts b/apps/meteor/client/sidebar/RoomList/normalizeSidebarMessage.ts
index f7cb2edd5ffe..9a506b861e56 100644
--- a/apps/meteor/client/sidebar/RoomList/normalizeSidebarMessage.ts
+++ b/apps/meteor/client/sidebar/RoomList/normalizeSidebarMessage.ts
@@ -1,12 +1,13 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { escapeHTML } from '@rocket.chat/string-helpers';
import type { useTranslation } from '@rocket.chat/ui-contexts';
+import emojione from 'emojione';
import { filterMarkdown } from '../../../app/markdown/lib/markdown';
export const normalizeSidebarMessage = (message: IMessage, t: ReturnType): string | undefined => {
if (message.msg) {
- return escapeHTML(filterMarkdown(message.msg));
+ return escapeHTML(filterMarkdown(emojione.shortnameToUnicode(message.msg)));
}
if (message.attachments) {
diff --git a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx
index 41e61b31eb6f..aba6617be232 100644
--- a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx
+++ b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx
@@ -1,5 +1,5 @@
import { css } from '@rocket.chat/css-in-js';
-import { Box, Divider, Palette, SidebarFooter as Footer } from '@rocket.chat/fuselage';
+import { Box, SidebarDivider, Palette, SidebarFooter as Footer } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import React from 'react';
@@ -20,7 +20,7 @@ const SidebarFooterDefault = (): ReactElement => {
return (
-
+
{t('Teams_New_Read_only_Label')}
@@ -192,7 +192,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement =>
/>
-
+
{t('Teams_New_Encrypted_Label')}
diff --git a/apps/meteor/client/sidebar/header/actions/CreateRoomList.tsx b/apps/meteor/client/sidebar/header/actions/CreateRoomList.tsx
index 7665fe999d42..f293762a1e4e 100644
--- a/apps/meteor/client/sidebar/header/actions/CreateRoomList.tsx
+++ b/apps/meteor/client/sidebar/header/actions/CreateRoomList.tsx
@@ -5,7 +5,7 @@ import React from 'react';
import CreateDiscussion from '../../../components/CreateDiscussion';
import ListItem from '../../../components/Sidebar/ListItem';
-import CreateChannelWithData from '../CreateChannelWithData';
+import CreateChannelWithData from '../CreateChannel';
import CreateDirectMessage from '../CreateDirectMessage';
import CreateTeam from '../CreateTeam';
import { useCreateRoomModal } from '../hooks/useCreateRoomModal';
diff --git a/apps/meteor/client/sidebar/header/index.tsx b/apps/meteor/client/sidebar/header/index.tsx
index d6370a5d791c..a8325d3899d7 100644
--- a/apps/meteor/client/sidebar/header/index.tsx
+++ b/apps/meteor/client/sidebar/header/index.tsx
@@ -3,7 +3,6 @@ import { useUser, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { memo } from 'react';
-import { useSidebarPaletteColor } from '../hooks/useSidebarPaletteColor';
import UserAvatarButton from './UserAvatarButton';
import Administration from './actions/Administration';
import CreateRoom from './actions/CreateRoom';
@@ -16,7 +15,6 @@ import Sort from './actions/Sort';
const HeaderWithData = (): ReactElement => {
const user = useUser();
const t = useTranslation();
- useSidebarPaletteColor();
return (
<>
diff --git a/apps/meteor/client/sidebar/hooks/useSidebarPaletteColor.ts b/apps/meteor/client/sidebar/hooks/useSidebarPaletteColor.ts
index 378661c032bb..ccf7d11c0e34 100644
--- a/apps/meteor/client/sidebar/hooks/useSidebarPaletteColor.ts
+++ b/apps/meteor/client/sidebar/hooks/useSidebarPaletteColor.ts
@@ -160,7 +160,7 @@ const getStyle = (
--rcx-color-foreground-hint: ${toVar(colors.n600)};
--rcx-sidebar-title-color: var(--rcx-color-neutral-400, ${toVar(colors.n400)});
- --rcx-sidebar-item-color: var(--rcx-color-neutral-400, ${toVar(colors.n400)});
+
--rcx-sidebar-item-color-hover: var(--rcx-color-neutral-400, ${toVar(colors.n400)});
--rcx-sidebar-item-color-selected: var(--rcx-color-neutral-400, ${toVar(colors.n400)});
--rcx-sidebar-footer-highlight-color: var(--rcx-color-neutral-400, ${toVar(colors.n400)});
@@ -174,9 +174,7 @@ const getStyle = (
--rcx-badge-colors-primary-background-color: ${toVar(colors.b500)}
}
- .rcx-sidebar {
- background-color: ${toVar(colors.sidebarSurface)};
- }
+
`
)(isIE11 ? ':root' : modifier);
diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx
index e63fe468a69c..cf4f2536e807 100644
--- a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx
+++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx
@@ -1,12 +1,12 @@
import { Sidebar } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useEndpoint, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
-import type { ReactElement } from 'react';
+import type { ReactElement, ComponentProps } from 'react';
import React from 'react';
import { useOmnichannelAgentAvailable } from '../../../hooks/omnichannel/useOmnichannelAgentAvailable';
-export const OmnichannelLivechatToggle = (): ReactElement => {
+export const OmnichannelLivechatToggle = (props: Omit, 'icon'>): ReactElement => {
const t = useTranslation();
const agentAvailable = useOmnichannelAgentAvailable();
const changeAgentStatus = useEndpoint('POST', '/v1/livechat/agent.status');
@@ -22,6 +22,7 @@ export const OmnichannelLivechatToggle = (): ReactElement => {
return (
import('./components/messag
createTemplateForComponent('BroadCastMetric', () => import('./components/message/Metrics/Broadcast'));
-createTemplateForComponent(
- 'Checkbox',
- async (): Promise<{ default: typeof CheckBox }> => {
- const { CheckBox } = await import('@rocket.chat/fuselage');
- return { default: CheckBox };
- },
- {
- attachment: 'at-parent',
- },
-);
-
createTemplateForComponent('UnreadMessagesIndicator', () => import('./views/room/components/body/UnreadMessagesIndicator'), {
attachment: 'at-parent',
});
diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxTable.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxTable.tsx
index 990d2df402dd..d324ea9845f5 100644
--- a/apps/meteor/client/views/admin/emailInbox/EmailInboxTable.tsx
+++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxTable.tsx
@@ -1,5 +1,6 @@
-import { Pagination, States, StatesIcon, StatesTitle } from '@rocket.chat/fuselage';
-import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
+import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage';
+import { useRoute, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
+import { useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import React, { useMemo, useCallback } from 'react';
@@ -14,39 +15,13 @@ import {
} from '../../../components/GenericTable';
import { usePagination } from '../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../components/GenericTable/hooks/useSort';
-import { useEndpointData } from '../../../hooks/useEndpointData';
-import { AsyncStatePhase } from '../../../lib/asyncState/AsyncStatePhase';
import SendTestButton from './SendTestButton';
-const useQuery = (
- {
- itemsPerPage,
- current,
- }: {
- itemsPerPage: number;
- current: number;
- },
- [column, direction]: string[],
-): {
- offset?: number;
- count?: number;
- sort: string;
-} =>
- useMemo(
- () => ({
- sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }),
- ...(itemsPerPage && { count: itemsPerPage }),
- ...(current && { offset: current }),
- }),
- [column, current, direction, itemsPerPage],
- );
-
const EmailInboxTable = (): ReactElement => {
const t = useTranslation();
const router = useRoute('admin-email-inboxes');
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const { sortBy, sortDirection, setSort } = useSort<'name' | 'email' | 'active'>('name');
- const query = useQuery({ itemsPerPage, current }, [sortBy, sortDirection]);
const onClick = useCallback(
(_id) => (): void => {
@@ -58,7 +33,15 @@ const EmailInboxTable = (): ReactElement => {
[router],
);
- const { phase, value: { emailInboxes = [], count = 0 } = {} } = useEndpointData('/v1/email-inbox.list', query);
+ const endpoint = useEndpoint('GET', '/v1/email-inbox.list');
+
+ const query = {
+ sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }),
+ ...(itemsPerPage && { count: itemsPerPage }),
+ ...(current && { offset: current }),
+ };
+
+ const result = useQuery(['email-list', query], () => endpoint(query));
const headers = useMemo(
() => [
@@ -78,18 +61,20 @@ const EmailInboxTable = (): ReactElement => {
return (
<>
- {phase === AsyncStatePhase.LOADING && (
+ {result.isLoading && (
{headers}
- {phase === AsyncStatePhase.LOADING && }
+
+
+
)}
- {emailInboxes && emailInboxes.length > 0 && phase === AsyncStatePhase.RESOLVED && (
+ {result.isSuccess && result.data.emailInboxes.length > 0 && (
<>
{headers}
- {emailInboxes.map((emailInbox) => (
+ {result.data.emailInboxes.map((emailInbox) => (
{
>
)}
- {phase === AsyncStatePhase.RESOLVED && emailInboxes.length === 0 && (
+ {result.isSuccess && result.data.emailInboxes.length === 0 && (
{t('No_results_found')}
)}
+
+ {result.isError && (
+
+
+ {t('Something_went_wrong')}
+
+ result.refetch()}>{t('Reload_page')}
+
+
+ )}
>
);
};
diff --git a/apps/meteor/client/views/admin/sidebar/UpgradeTab.tsx b/apps/meteor/client/views/admin/sidebar/UpgradeTab.tsx
index bb9a14292b2a..f5c67ede0eec 100644
--- a/apps/meteor/client/views/admin/sidebar/UpgradeTab.tsx
+++ b/apps/meteor/client/views/admin/sidebar/UpgradeTab.tsx
@@ -1,5 +1,4 @@
import { Box, Icon } from '@rocket.chat/fuselage';
-import colors from '@rocket.chat/fuselage-tokens/colors';
import { useRoutePath, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useMemo } from 'react';
@@ -9,12 +8,6 @@ import { getUpgradeTabLabel, isFullyFeature } from '../../../../lib/upgradeTab';
import Emoji from '../../../components/Emoji';
import Sidebar from '../../../components/Sidebar';
-const customColors = {
- default: colors['s2-700'],
- hover: colors['s2-800'],
- active: colors['s2-900'],
-};
-
type UpgradeTabProps = { type: UpgradeTabVariant; currentPath: string; trialEndDate: string | undefined };
const UpgradeTab = ({ type, currentPath, trialEndDate }: UpgradeTabProps): ReactElement => {
@@ -34,9 +27,9 @@ const UpgradeTab = ({ type, currentPath, trialEndDate }: UpgradeTabProps): React
const displayEmoji = isFullyFeature(type);
return (
-
+
-
+
{t(label)} {displayEmoji && }
diff --git a/apps/meteor/client/views/home/DefaultHomePage.tsx b/apps/meteor/client/views/home/DefaultHomePage.tsx
index d0215d940b22..e5a22c82cd5a 100644
--- a/apps/meteor/client/views/home/DefaultHomePage.tsx
+++ b/apps/meteor/client/views/home/DefaultHomePage.tsx
@@ -28,7 +28,7 @@ const DefaultHomePage = (): ReactElement => {
-
+
{t('Welcome_to', { Site_Name: workspaceName || 'Rocket.Chat' })}
@@ -59,7 +59,7 @@ const DefaultHomePage = (): ReactElement => {
{displayCustomBody && (
-
+
)}
diff --git a/apps/meteor/client/views/home/HomepageGridItem.tsx b/apps/meteor/client/views/home/HomepageGridItem.tsx
index e61784320153..95a28f7903d0 100644
--- a/apps/meteor/client/views/home/HomepageGridItem.tsx
+++ b/apps/meteor/client/views/home/HomepageGridItem.tsx
@@ -9,7 +9,7 @@ const HomepageGridItem = ({ children }: { children: ReactNode }): ReactElement =
const isMedium = !breakpoints.includes('lg');
return (
-
+
{children}
);
diff --git a/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx b/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx
index 09ce0192a83c..0c67229e61d0 100644
--- a/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx
+++ b/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx
@@ -4,7 +4,7 @@ import { useTranslation, useSetModal } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';
-import CreateChannelWithData from '../../../sidebar/header/CreateChannelWithData';
+import CreateChannelWithData from '../../../sidebar/header/CreateChannel';
const CreateChannelsCard = (): ReactElement => {
const t = useTranslation();
diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx
index 01640818b2ba..7f86fb06e463 100644
--- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx
+++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx
@@ -26,44 +26,50 @@ import RemoveChatButton from './RemoveChatButton';
import { useAllCustomFields } from './hooks/useAllCustomFields';
import { useCurrentChats } from './hooks/useCurrentChats';
+type DebouncedParams = {
+ fname: string;
+ guest: string;
+ servedBy: string;
+ department: string;
+ status: string;
+ from: string;
+ to: string;
+ tags: any[];
+};
+
+type CurrentChatQuery = {
+ agents?: string[];
+ offset?: number;
+ roomName?: string;
+ departmentId?: string;
+ open?: boolean;
+ createdAt?: string;
+ closedAt?: string;
+ tags?: string[];
+ onhold?: boolean;
+ customFields?: string;
+ sort: string;
+ count?: number;
+};
+
type useQueryType = (
- debouncedParams: {
- fname: string;
- guest: string;
- servedBy: string;
- department: string;
- status: string;
- from: string;
- to: string;
- tags: any[];
- itemsPerPage: 25 | 50 | 100;
- current: number;
- },
+ debouncedParams: DebouncedParams,
customFields: { [key: string]: string } | undefined,
[column, direction]: [string, 'asc' | 'desc'],
+ current: number,
+ itemsPerPage: 25 | 50 | 100,
) => GETLivechatRoomsParams;
const sortDir = (sortDir: 'asc' | 'desc'): 1 | -1 => (sortDir === 'asc' ? 1 : -1);
const currentChatQuery: useQueryType = (
- { guest, servedBy, department, status, from, to, tags, itemsPerPage, current },
+ { guest, servedBy, department, status, from, to, tags },
customFields,
[column, direction],
+ current,
+ itemsPerPage,
) => {
- const query: {
- agents?: string[];
- offset?: number;
- roomName?: string;
- departmentId?: string;
- open?: boolean;
- createdAt?: string;
- closedAt?: string;
- tags?: string[];
- onhold?: boolean;
- customFields?: string;
- sort: string;
- count?: number;
- } = {
+ const query: CurrentChatQuery = {
...(guest && { roomName: guest }),
sort: JSON.stringify({
[column]: sortDir(direction),
@@ -112,44 +118,50 @@ const currentChatQuery: useQueryType = (
const CurrentChatsRoute = (): ReactElement => {
const { sortBy, sortDirection, setSort } = useSort<'fname' | 'departmentId' | 'servedBy' | 'ts' | 'lm' | 'open'>('ts', 'desc');
const [customFields, setCustomFields] = useState<{ [key: string]: string }>();
- const [params, setParams] = useState({
- guest: '',
- fname: '',
- servedBy: '',
- status: 'all',
- department: '',
- from: '',
- to: '',
- current: 0,
- itemsPerPage: 25 as 25 | 50 | 100,
- tags: [] as string[],
- });
+
const t = useTranslation();
const id = useRouteParameter('id');
- const query = useMemo(
- () => currentChatQuery(params, customFields, [sortBy, sortDirection]),
- [customFields, params, sortBy, sortDirection],
- );
const canViewCurrentChats = usePermission('view-livechat-current-chats');
const canRemoveClosedChats = usePermission('remove-closed-livechat-room');
const directoryRoute = useRoute('omnichannel-current-chats');
- const result = useCurrentChats(query);
-
const { data: allCustomFields } = useAllCustomFields();
const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination();
+ const [params, setParams] = useState({
+ guest: '',
+ fname: '',
+ servedBy: '',
+ status: 'all',
+ department: '',
+ from: '',
+ to: '',
+ tags: [] as string[],
+ });
+
const hasCustomFields = useMemo(
() => !!allCustomFields?.customFields?.find((customField) => customField.scope === 'room'),
[allCustomFields],
);
+ const query = useMemo(
+ () => currentChatQuery(params, customFields, [sortBy, sortDirection], current, itemsPerPage),
+ [customFields, itemsPerPage, params, sortBy, sortDirection, current],
+ );
+
+ const result = useCurrentChats(query);
+
const onRowClick = useMutableCallback((_id) => {
directoryRoute.push({ id: _id });
});
+ const onFilter = useMutableCallback((params: DebouncedParams): void => {
+ setParams(params);
+ setCurrent(0);
+ });
+
const renderRow = useCallback(
({ _id, fname, servedBy, ts, lm, department, open, onHold }) => {
const getStatusText = (open: boolean, onHold: boolean): string => {
@@ -196,7 +208,7 @@ const CurrentChatsRoute = (): ReactElement => {
['setFilter']}
+ setFilter={onFilter as ComponentProps['setFilter']}
setCustomFields={setCustomFields}
customFields={customFields}
hasCustomFields={hasCustomFields}
diff --git a/apps/meteor/client/views/room/Announcement/Announcement.tsx b/apps/meteor/client/views/room/Announcement/Announcement.tsx
index 8134e4aa2d47..052aabfa2f55 100644
--- a/apps/meteor/client/views/room/Announcement/Announcement.tsx
+++ b/apps/meteor/client/views/room/Announcement/Announcement.tsx
@@ -31,7 +31,7 @@ const Announcement: FC = ({ announcement, announcementDetail
: setModal(
-
+
,
);
@@ -39,7 +39,7 @@ const Announcement: FC = ({ announcement, announcementDetail
return announcement ? (
): void => handleClick(e)}>
-
+
) : null;
};
diff --git a/apps/meteor/client/views/room/MessageList/components/Toolbox/MessageActionMenu.tsx b/apps/meteor/client/views/room/MessageList/components/Toolbox/MessageActionMenu.tsx
index fd103cda3f59..439d257569b8 100644
--- a/apps/meteor/client/views/room/MessageList/components/Toolbox/MessageActionMenu.tsx
+++ b/apps/meteor/client/views/room/MessageList/components/Toolbox/MessageActionMenu.tsx
@@ -38,7 +38,10 @@ export const MessageActionMenu: FC<{
setVisible(!visible)}
+ onClick={(e): void => {
+ e.stopPropagation();
+ setVisible(!visible);
+ }}
data-qa-id='menu'
data-qa-type='message-action-menu'
title={t('More')}
diff --git a/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx b/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx
index 06591e9d877d..d65120337923 100644
--- a/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx
+++ b/apps/meteor/client/views/room/MessageList/components/Toolbox/Toolbox.tsx
@@ -54,10 +54,7 @@ export const Toolbox: FC<{ message: IMessage }> = ({ message }) => {
{messageActions.map((action) => (
{
- e.stopPropagation();
- action.action(e, { message, tabbar: toolbox, room, chat });
- }}
+ onClick={(e): void => action.action(e, { message, tabbar: toolbox, room, chat })}
key={action.id}
icon={action.icon}
title={t(action.label)}
@@ -69,10 +66,7 @@ export const Toolbox: FC<{ message: IMessage }> = ({ message }) => {
({
...action,
- action: (e): void => {
- e.stopPropagation();
- action.action(e, { message, tabbar: toolbox, room, chat });
- },
+ action: (e): void => action.action(e, { message, tabbar: toolbox, room, chat }),
}))}
data-qa-type='message-action-menu-options'
/>
diff --git a/apps/meteor/client/views/room/components/body/LeaderBar.tsx b/apps/meteor/client/views/room/components/body/LeaderBar.tsx
index 1392597e04f7..cc40d68ef373 100644
--- a/apps/meteor/client/views/room/components/body/LeaderBar.tsx
+++ b/apps/meteor/client/views/room/components/body/LeaderBar.tsx
@@ -1,25 +1,29 @@
import type { IUser } from '@rocket.chat/core-typings';
+import { css } from '@rocket.chat/css-in-js';
+import { Box, Button } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
-import type { ReactElement, ReactNode, UIEvent } from 'react';
+import type { ReactElement, UIEvent } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { isTruthy } from '../../../../../lib/isTruthy';
+import { UserStatus } from '../../../../components/UserStatus';
import UserAvatar from '../../../../components/avatar/UserAvatar';
+import { usePresence } from '../../../../hooks/usePresence';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
type LeaderBarProps = {
+ _id: IUser['_id'];
name: IUser['name'];
username: IUser['username'];
- status?: 'online' | 'offline' | 'busy' | 'away';
- statusText?: ReactNode;
visible: boolean;
onAvatarClick?: (event: UIEvent, username: IUser['username']) => void;
};
-const LeaderBar = ({ name, username, status = 'offline', statusText, visible, onAvatarClick }: LeaderBarProps): ReactElement => {
+const LeaderBar = ({ _id, name, username, visible, onAvatarClick }: LeaderBarProps): ReactElement => {
const t = useTranslation();
const chatNowLink = useMemo(() => roomCoordinator.getRouteLink('d', { name: username }) || undefined, [username]);
+ const roomLeaderData = usePresence(_id);
const handleAvatarClick = useCallback(
(event: UIEvent) => {
@@ -32,31 +36,51 @@ const LeaderBar = ({ name, username, status = 'offline', statusText, visible, on
throw new Error('username is required');
}
+ const roomLeaderStyle = css`
+ position: absolute;
+ z-index: 9;
+ right: 0;
+ left: 0;
+
+ visibility: visible;
+
+ transition: transform 0.15s cubic-bezier(0.5, 0, 0.1, 1), visibility 0.15s cubic-bezier(0.5, 0, 0.1, 1);
+
+ &.animated-hidden {
+ visibility: hidden;
+
+ transform: translateY(-100%);
+ }
+ `;
+
return (
-
+
+
);
};
diff --git a/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx b/apps/meteor/client/views/room/components/body/LegacyMessageTemplateList.tsx
index 6831f2df7901..5756821cc7d3 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/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx
index 122ea38b8500..fb585209ba98 100644
--- a/apps/meteor/client/views/room/components/body/RoomBody.tsx
+++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx
@@ -80,7 +80,7 @@ const RoomBody = (): ReactElement => {
throw new Error('No ChatContext provided');
}
- const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget(room);
+ const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget();
const _isAtBottom = useCallback((scrollThreshold = 0) => {
const wrapper = wrapperRef.current;
@@ -160,7 +160,7 @@ const RoomBody = (): ReactElement => {
const useRealName = useSetting('UI_Use_Real_Name') as boolean;
- const { data: roomLeader } = useReactiveQuery(['rooms', room._id, 'leader', { not: user?._id }], ({ roomRoles, users }) => {
+ const { data: roomLeader } = useReactiveQuery(['rooms', room._id, 'leader', { not: user?._id }], ({ roomRoles }) => {
const leaderRoomRole = roomRoles.findOne({
'rid': room._id,
'roles': 'leader',
@@ -171,13 +171,9 @@ const RoomBody = (): ReactElement => {
return null;
}
- const leaderUser = users.findOne({ _id: leaderRoomRole.u._id }, { fields: { status: 1, statusText: 1 } });
-
return {
...leaderRoomRole.u,
name: useRealName ? leaderRoomRole.u.name || leaderRoomRole.u.username : leaderRoomRole.u.username,
- status: leaderUser?.status,
- statusText: leaderUser?.statusText,
};
});
@@ -614,10 +610,9 @@ const RoomBody = (): ReactElement => {
) : null}
{roomLeader ? (
diff --git a/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx b/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx
index 2e4c0fd48bdd..0c10a217ce5b 100644
--- a/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx
+++ b/apps/meteor/client/views/room/components/body/composer/ComposerMessage.tsx
@@ -19,6 +19,7 @@ export type ComposerMessageProps = {
chatMessagesInstance: ContextType;
onResize?: () => void;
onEscape?: () => void;
+ onSend?: () => void;
onNavigateToNextMessage?: () => void;
onNavigateToPreviousMessage?: () => void;
onUploadFiles?: (files: readonly File[]) => void;
@@ -30,6 +31,7 @@ const ComposerMessage = ({
chatMessagesInstance,
onResize,
onEscape,
+ onSend,
onNavigateToNextMessage,
onNavigateToPreviousMessage,
onUploadFiles,
@@ -37,6 +39,32 @@ const ComposerMessage = ({
const isLayoutEmbedded = useEmbeddedLayout();
const showFormattingTips = useSetting('Message_ShowFormattingTips') as boolean;
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ const handleSend = useCallback(
+ async (
+ _event: Event,
+ {
+ value: text,
+ tshow,
+ }: {
+ value: string;
+ tshow?: boolean;
+ },
+ ): Promise => {
+ try {
+ const newMessageSent = await chatMessagesInstance?.flows.sendMessage({
+ text,
+ tshow,
+ });
+ if (newMessageSent) onSend?.();
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error });
+ }
+ },
+ [chatMessagesInstance?.flows, dispatchToastMessage, onSend],
+ );
+
const messageBoxViewRef = useRef();
const messageBoxViewDataRef = useRef(
new ReactiveVar({
@@ -46,6 +74,7 @@ const ComposerMessage = ({
showFormattingTips: showFormattingTips && !isLayoutEmbedded,
onResize,
onEscape,
+ onSend: handleSend,
onNavigateToNextMessage,
onNavigateToPreviousMessage,
onUploadFiles,
@@ -61,6 +90,7 @@ const ComposerMessage = ({
showFormattingTips: showFormattingTips && !isLayoutEmbedded,
onResize,
onEscape,
+ onSend: handleSend,
onNavigateToNextMessage,
onNavigateToPreviousMessage,
onUploadFiles,
@@ -77,49 +107,20 @@ const ComposerMessage = ({
onNavigateToNextMessage,
onNavigateToPreviousMessage,
onUploadFiles,
+ handleSend,
]);
- const dispatchToastMessage = useToastMessageDispatch();
-
- const footerRef = useCallback(
- (footer: HTMLElement | null) => {
- if (footer) {
- messageBoxViewRef.current = Blaze.renderWithData(
- Template.messageBox,
- (): MessageBoxTemplateInstance['data'] => ({
- ...messageBoxViewDataRef.current.get(),
- onSend: async (
- _event: Event,
- {
- value: text,
- tshow,
- }: {
- value: string;
- tshow?: boolean;
- },
- ): Promise => {
- try {
- await chatMessagesInstance?.flows.sendMessage({
- text,
- tshow,
- });
- } catch (error) {
- dispatchToastMessage({ type: 'error', message: error });
- }
- },
- }),
- footer,
- );
- return;
- }
+ const footerRef = useCallback((footer: HTMLElement | null) => {
+ if (footer) {
+ messageBoxViewRef.current = Blaze.renderWithData(Template.messageBox, () => messageBoxViewDataRef.current.get(), footer);
+ return;
+ }
- if (messageBoxViewRef.current) {
- Blaze.remove(messageBoxViewRef.current);
- messageBoxViewRef.current = undefined;
- }
- },
- [chatMessagesInstance, dispatchToastMessage],
- );
+ if (messageBoxViewRef.current) {
+ Blaze.remove(messageBoxViewRef.current);
+ messageBoxViewRef.current = undefined;
+ }
+ }, []);
const publicationReady = useReactiveValue(useCallback(() => RoomManager.getOpenedRoomByRid(rid)?.streamActive ?? false, [rid]));
diff --git a/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts
index 21e043928a9c..bd89a3d72a60 100644
--- a/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts
+++ b/apps/meteor/client/views/room/components/body/useFileUploadDropTarget.ts
@@ -1,19 +1,16 @@
-import type { IRoom } from '@rocket.chat/core-typings';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useSetting, useTranslation } from '@rocket.chat/ui-contexts';
+import { useSetting, useTranslation, useUser } from '@rocket.chat/ui-contexts';
import type { ReactNode } from 'react';
import type React from 'react';
import { useCallback, useMemo } from 'react';
-import { Users } from '../../../../../app/models/client';
import { useReactiveValue } from '../../../../hooks/useReactiveValue';
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
import { useChat } from '../../contexts/ChatContext';
+import { useRoom } from '../../contexts/RoomContext';
import { useDropTarget } from './useDropTarget';
-export const useFileUploadDropTarget = (
- room: IRoom,
-): readonly [
+export const useFileUploadDropTarget = (): readonly [
fileUploadTriggerProps: {
onDragEnter: (event: React.DragEvent) => void;
},
@@ -24,16 +21,15 @@ export const useFileUploadDropTarget = (
reason?: ReactNode;
},
] => {
+ const room = useRoom();
const { triggerProps, overlayProps } = useDropTarget();
const t = useTranslation();
const fileUploadEnabled = useSetting('FileUpload_Enabled') as boolean;
+ const user = useUser();
const fileUploadAllowedForUser = useReactiveValue(
- useCallback(
- () => !roomCoordinator.readOnly(room._id, Users.findOne({ _id: Meteor.userId() }, { fields: { username: 1 } })),
- [room._id],
- ),
+ useCallback(() => !roomCoordinator.readOnly(room._id, { username: user?.username }), [room._id, user?.username]),
);
const chat = useChat();
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 63cd5ccf8f63..04867f43b8d6 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/Thread.tsx b/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx
new file mode 100644
index 000000000000..0c127558bc68
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx
@@ -0,0 +1,134 @@
+import type { IMessage } from '@rocket.chat/core-typings';
+import { css } from '@rocket.chat/css-in-js';
+import { Box, Modal, Skeleton } from '@rocket.chat/fuselage';
+import { useLocalStorage } from '@rocket.chat/fuselage-hooks';
+import { useLayoutContextualBarExpanded, useToastMessageDispatch, useTranslation, useUserId } from '@rocket.chat/ui-contexts';
+import type { ReactElement } from 'react';
+import React from 'react';
+
+import VerticalBar from '../../../../components/VerticalBar';
+import { useRoom, useRoomSubscription } from '../../contexts/RoomContext';
+import { useTabBarClose } from '../../contexts/ToolboxContext';
+import ChatProvider from '../../providers/ChatProvider';
+import MessageProvider from '../../providers/MessageProvider';
+import ThreadChat from './components/ThreadChat';
+import ThreadSkeleton from './components/ThreadSkeleton';
+import ThreadTitle from './components/ThreadTitle';
+import { useGoToThreadList } from './hooks/useGoToThreadList';
+import { useThreadMainMessageQuery } from './hooks/useThreadMainMessageQuery';
+import { useToggleFollowingThreadMutation } from './hooks/useToggleFollowingThreadMutation';
+
+type ThreadProps = {
+ tmid: IMessage['_id'];
+};
+
+const Thread = ({ tmid }: ThreadProps): ReactElement => {
+ const goToThreadList = useGoToThreadList();
+ const closeTabBar = useTabBarClose();
+
+ const mainMessageQueryResult = useThreadMainMessageQuery(tmid, {
+ onDelete: () => {
+ closeTabBar();
+ },
+ });
+
+ const room = useRoom();
+ const subscription = useRoomSubscription();
+
+ const t = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ const canExpand = useLayoutContextualBarExpanded();
+ const [expanded, setExpanded] = useLocalStorage('expand-threads', false);
+
+ const uid = useUserId();
+ const following = uid ? mainMessageQueryResult.data?.replies?.includes(uid) ?? false : false;
+ const toggleFollowingMutation = useToggleFollowingThreadMutation({
+ onError: (error) => {
+ dispatchToastMessage({ type: 'error', message: error });
+ },
+ });
+
+ const handleBackdropClick = () => {
+ closeTabBar();
+ };
+
+ const handleGoBack = () => {
+ goToThreadList();
+ };
+
+ const handleToggleExpand = () => {
+ setExpanded((expanded) => !expanded);
+ };
+
+ const handleToggleFollowing = () => {
+ toggleFollowingMutation.mutate({ tmid, follow: !following });
+ };
+
+ const handleClose = () => {
+ closeTabBar();
+ };
+
+ return (
+
+ {canExpand && expanded && }
+
+
+
+
+ {(mainMessageQueryResult.isLoading && ) ||
+ (mainMessageQueryResult.isSuccess && ) ||
+ null}
+
+ {canExpand && (
+
+ )}
+
+
+
+
+
+ {(mainMessageQueryResult.isLoading && ) ||
+ (mainMessageQueryResult.isSuccess && (
+
+
+
+
+
+ )) ||
+ null}
+
+
+
+ );
+};
+
+export default Thread;
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx
index ef7251173472..6534e6accbd5 100644
--- a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx
+++ b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx
@@ -1,94 +1,48 @@
-import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
+import type { IMessage } from '@rocket.chat/core-typings';
import { Box, Icon, TextInput, Select, Margins, Callout, Throbber } from '@rocket.chat/fuselage';
-import { useResizeObserver, useMutableCallback, useAutoFocus } from '@rocket.chat/fuselage-hooks';
-import {
- useRoute,
- useCurrentRoute,
- useQueryStringParameter,
- useSetting,
- useTranslation,
- useUserSubscription,
-} from '@rocket.chat/ui-contexts';
-import type { FC } from 'react';
-import React, { useMemo } from 'react';
+import { useResizeObserver, useAutoFocus, useLocalStorage, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
+import { useTranslation, useUserId } from '@rocket.chat/ui-contexts';
+import type { FormEvent, ReactElement } from 'react';
+import React, { useMemo, useState, useCallback } from 'react';
import { Virtuoso } from 'react-virtuoso';
import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper';
import VerticalBar from '../../../../components/VerticalBar';
-import { useTabContext } from '../../contexts/ToolboxContext';
-import ChatProvider from '../../providers/ChatProvider';
-import MessageProvider from '../../providers/MessageProvider';
-import ThreadComponent from '../../threads/ThreadComponent';
-import ThreadRow from './ThreadRow';
-import { withData } from './withData';
-
-export type ThreadListProps = {
- total: number;
- threads: IMessage[];
- room: IRoom;
- unread?: string[];
- unreadUser?: string[];
- unreadGroup?: string[];
- userId?: IUser['_id'] | null;
-
- type: 'all' | 'following' | 'unread';
- setType: (type: string) => void;
-
- loading: boolean;
-
- error?: Error;
+import { useRecordList } from '../../../../hooks/lists/useRecordList';
+import { AsyncStatePhase } from '../../../../lib/asyncState';
+import type { ThreadsListOptions } from '../../../../lib/lists/ThreadsList';
+import { useRoom, useRoomSubscription } from '../../contexts/RoomContext';
+import { useTabBarClose } from '../../contexts/ToolboxContext';
+import ThreadListItem from './components/ThreadListItem';
+import { useGoToThread } from './hooks/useGoToThread';
+import { useThreadsList } from './hooks/useThreadsList';
+
+type ThreadType = 'all' | 'following' | 'unread';
+
+const ThreadList = (): ReactElement => {
+ const t = useTranslation();
- text: string;
- setText: (text: string) => void;
+ const closeTabBar = useTabBarClose();
+ const handleTabBarCloseButtonClick = useCallback(() => {
+ closeTabBar();
+ }, [closeTabBar]);
- onClose: () => void;
+ const { ref, contentBoxSize: { inlineSize = 378, blockSize = 1 } = {} } = useResizeObserver({
+ debounceDelay: 200,
+ });
- loadMoreItems: (min: number, max: number) => void;
-};
+ const autoFocusRef = useAutoFocus(true);
-const subscriptionFields = {};
-
-const ThreadList: FC = function ThreadList({
- total = 10,
- threads = [],
- room,
- unread = [],
- unreadUser = [],
- unreadGroup = [],
- text,
- type,
- setType,
- loadMoreItems,
- loading,
- onClose,
- error,
- userId = '',
- setText,
-}) {
- const subscription = useUserSubscription(room._id, subscriptionFields);
-
- const showRealNames = Boolean(useSetting('UI_Use_Real_Name'));
+ const [searchText, setSearchText] = useState('');
- const t = useTranslation();
- const inputRef = useAutoFocus(true);
- const [name] = useCurrentRoute();
-
- if (!name) {
- throw new Error('No route name');
- }
-
- const channelRoute = useRoute(name);
- const onClick = useMutableCallback((e) => {
- const { id: context } = e.currentTarget.dataset;
- channelRoute.push({
- tab: 'thread',
- context,
- rid: room._id,
- ...(room.name && { name: room.name }),
- });
- });
+ const handleSearchTextChange = useCallback(
+ (event: FormEvent) => {
+ setSearchText(event.currentTarget.value);
+ },
+ [setSearchText],
+ );
- const options: [string, string][] = useMemo(
+ const typeOptions: (readonly [type: ThreadType, label: string])[] = useMemo(
() => [
['all', t('All')],
['following', t('Following')],
@@ -97,104 +51,146 @@ const ThreadList: FC = function ThreadList({
[t],
);
- const { ref, contentBoxSize: { inlineSize = 378, blockSize = 1 } = {} } = useResizeObserver({
- debounceDelay: 200,
- });
+ const [type, setType] = useLocalStorage('thread-list-type', 'all');
+
+ const handleTypeChange = useCallback(
+ (type: string) => {
+ const typeOption = typeOptions.find(([t]) => t === type);
+ if (typeOption) setType(typeOption[0]);
+ },
+ [setType, typeOptions],
+ );
+
+ const room = useRoom();
+ const rid = room._id;
+ const subscription = useRoomSubscription();
+ const subscribed = !!subscription;
+ const uid = useUserId();
+ const tunread = subscription?.tunread?.sort().join(',');
+ const text = useDebouncedValue(searchText, 400);
+ const options: ThreadsListOptions = useDebouncedValue(
+ useMemo(() => {
+ if (type === 'all' || !subscribed || !uid) {
+ return {
+ rid,
+ text,
+ type: 'all',
+ };
+ }
+ switch (type) {
+ case 'following':
+ return {
+ rid,
+ text,
+ type,
+ uid,
+ };
+ case 'unread':
+ return {
+ rid,
+ text,
+ type,
+ tunread: tunread?.split(','),
+ };
+ }
+ }, [rid, subscribed, text, tunread, type, uid]),
+ 300,
+ );
+
+ const { threadsList, loadMoreItems } = useThreadsList(options, uid);
+ const { phase, error, items, itemCount } = useRecordList(threadsList);
- const mid = useTabContext();
- const jump = useQueryStringParameter('jump');
+ const goToThread = useGoToThread();
+ const handleThreadClick = useCallback(
+ (tmid: IMessage['_id']) => {
+ goToThread(tmid);
+ },
+ [goToThread],
+ );
return (
<>
{t('Threads')}
-
+
-
-
+
+
}
- ref={inputRef}
+ ref={autoFocusRef}
+ value={searchText}
+ onChange={handleSearchTextChange}
/>
-
+
- {loading && (
-
-
+ {phase === AsyncStatePhase.LOADING && (
+
+
)}
{error && (
-
+
{error.toString()}
)}
- {!loading && total === 0 && (
-
+ {phase !== AsyncStatePhase.LOADING && itemCount === 0 && (
+
{t('No_Threads')}
)}
- {!error && total > 0 && threads.length > 0 && (
+ {!error && itemCount > 0 && items.length > 0 && (
undefined : (start): unknown => loadMoreItems(start, Math.min(50, total - start))}
- overscan={25}
- data={threads}
- components={{ Scroller: ScrollableContentWrapper as any }}
- itemContent={(_index, data: IMessage): FC =>
- (
-
- ) as unknown as FC
+ totalCount={itemCount}
+ endReached={
+ phase === AsyncStatePhase.LOADING
+ ? (): void => undefined
+ : (start): void => {
+ loadMoreItems(start, Math.min(50, itemCount - start));
+ }
}
+ overscan={25}
+ data={items}
+ components={{ Scroller: ScrollableContentWrapper }}
+ itemContent={(_index, data: IMessage): ReactElement => (
+
+ )}
/>
)}
-
- {typeof mid === 'string' && (
-
-
-
-
-
-
-
- )}
>
);
};
-export default withData(ThreadList);
+export default ThreadList;
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/ThreadRow.tsx b/apps/meteor/client/views/room/contextualBar/Threads/ThreadRow.tsx
deleted file mode 100644
index e7447dd0857c..000000000000
--- a/apps/meteor/client/views/room/contextualBar/Threads/ThreadRow.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { IMessage } from '@rocket.chat/core-typings';
-import type { MouseEvent, ReactElement } from 'react';
-import React, { memo } from 'react';
-
-import { useDecryptedMessage } from '../../../../hooks/useDecryptedMessage';
-import { clickableItem } from '../../../../lib/clickableItem';
-import { normalizeThreadMessage } from '../../../../lib/normalizeThreadMessage';
-import { callWithErrorHandling } from '../../../../lib/utils/callWithErrorHandling';
-import ThreadListMessage from './components/ThreadListMessage';
-import { mapProps } from './mapProps';
-
-const Thread = memo(mapProps(clickableItem(ThreadListMessage)));
-
-const handleFollowButton = (e: MouseEvent, threadId: string): void => {
- e.preventDefault();
- e.stopPropagation();
- const { following } = e.currentTarget.dataset;
-
- following &&
- callWithErrorHandling(![true, 'true'].includes(following) ? 'followMessage' : 'unfollowMessage', {
- mid: threadId,
- });
-};
-
-type ThreadRowProps = {
- thread: IMessage;
- showRealNames: boolean;
- unread: string[];
- unreadUser: string[];
- unreadGroup: string[];
- userId: string;
- onClick: (threadId: string) => void;
-};
-
-function ThreadRow({ thread, showRealNames, unread, unreadUser, unreadGroup, userId, onClick }: ThreadRowProps): ReactElement {
- const decryptedMsg = useDecryptedMessage(thread);
- const msg = normalizeThreadMessage({ ...thread, msg: decryptedMsg });
-
- const { name = thread.u.username } = thread.u;
-
- return (
- ): unknown => handleFollowButton(e, thread._id)}
- onClick={onClick}
- />
- );
-}
-
-export default memo(ThreadRow);
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/Threads.tsx b/apps/meteor/client/views/room/contextualBar/Threads/Threads.tsx
new file mode 100644
index 000000000000..fe33cfa89ccc
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/Threads.tsx
@@ -0,0 +1,18 @@
+import type { ReactElement } from 'react';
+import React from 'react';
+
+import { useTabContext } from '../../contexts/ToolboxContext';
+import Thread from './Thread';
+import ThreadList from './ThreadList';
+
+const Threads = (): ReactElement => {
+ const tmid = useTabContext() as string | undefined;
+
+ if (tmid) {
+ return ;
+ }
+
+ return ;
+};
+
+export default Threads;
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/LegacyThreadMessageList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/LegacyThreadMessageList.tsx
new file mode 100644
index 000000000000..c4e0b0295f3e
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/components/LegacyThreadMessageList.tsx
@@ -0,0 +1,58 @@
+import type { IMessage } from '@rocket.chat/core-typings';
+import { useMergedRefs } from '@rocket.chat/fuselage-hooks';
+import { useUserPreference } from '@rocket.chat/ui-contexts';
+import type { ReactElement } from 'react';
+import React from 'react';
+
+import { isTruthy } from '../../../../../../lib/isTruthy';
+import LoadingMessagesIndicator from '../../../components/body/LoadingMessagesIndicator';
+import { useLegacyThreadMessageJump } from '../hooks/useLegacyThreadMessageJump';
+import { useLegacyThreadMessageListScrolling } from '../hooks/useLegacyThreadMessageListScrolling';
+import { useLegacyThreadMessageRef } from '../hooks/useLegacyThreadMessageRef';
+import { useLegacyThreadMessages } from '../hooks/useLegacyThreadMessages';
+
+type LegacyThreadMessageListProps = {
+ mainMessage: IMessage;
+ jumpTo?: string;
+ onJumpTo?: (mid: IMessage['_id']) => void;
+};
+
+const LegacyThreadMessageList = function LegacyThreadChatList({
+ mainMessage,
+ jumpTo,
+ onJumpTo,
+}: LegacyThreadMessageListProps): ReactElement {
+ const { messages, loading } = useLegacyThreadMessages(mainMessage._id);
+ const messageRef = useLegacyThreadMessageRef();
+ const { listWrapperRef: listWrapperScrollRef, listRef: listScrollRef, onScroll: handleScroll } = useLegacyThreadMessageListScrolling();
+ const { parentRef: listJumpRef } = useLegacyThreadMessageJump(jumpTo, { enabled: !loading, onJumpTo });
+
+ const listRef = useMergedRefs(listScrollRef, listJumpRef);
+ const hideUsernames = useUserPreference('hideUsernames');
+
+ return (
+
+
+ {loading ? (
+ -
+
+
+ ) : (
+ <>
+
+ {messages.map((message, index) => (
+
+ ))}
+ >
+ )}
+
+
+ );
+};
+
+export default LegacyThreadMessageList;
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx
new file mode 100644
index 000000000000..a7e698862734
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx
@@ -0,0 +1,141 @@
+import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings';
+import { isEditedMessage } from '@rocket.chat/core-typings';
+import { CheckBox } from '@rocket.chat/fuselage';
+import { useUniqueId } from '@rocket.chat/fuselage-hooks';
+import { useCurrentRoute, useMethod, useQueryStringParameter, useRoute, useTranslation, useUserPreference } from '@rocket.chat/ui-contexts';
+import type { ReactElement } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
+
+import { callbacks } from '../../../../../../lib/callbacks';
+import VerticalBar from '../../../../../components/VerticalBar';
+import DropTargetOverlay from '../../../components/body/DropTargetOverlay';
+import ComposerContainer from '../../../components/body/composer/ComposerContainer';
+import { useFileUploadDropTarget } from '../../../components/body/useFileUploadDropTarget';
+import { useChat } from '../../../contexts/ChatContext';
+import { useRoom, useRoomSubscription } from '../../../contexts/RoomContext';
+import { useTabBarClose } from '../../../contexts/ToolboxContext';
+import LegacyThreadMessageList from './LegacyThreadMessageList';
+
+type ThreadChatProps = {
+ mainMessage: IThreadMainMessage;
+};
+
+const ThreadChat = ({ mainMessage }: ThreadChatProps): ReactElement => {
+ const [fileUploadTriggerProps, fileUploadOverlayProps] = useFileUploadDropTarget();
+
+ const sendToChannelPreference = useUserPreference<'always' | 'never' | 'default'>('alsoSendThreadToChannel');
+
+ const [sendToChannel, setSendToChannel] = useState(() => {
+ switch (sendToChannelPreference) {
+ case 'always':
+ return true;
+ case 'never':
+ return false;
+ default:
+ return !mainMessage.tcount;
+ }
+ });
+
+ const handleSend = useCallback((): void => {
+ if (sendToChannelPreference === 'default') {
+ setSendToChannel(false);
+ }
+ }, [sendToChannelPreference]);
+
+ const closeTabBar = useTabBarClose();
+ const handleComposerEscape = useCallback((): void => {
+ closeTabBar();
+ }, [closeTabBar]);
+
+ const chat = useChat();
+
+ const handleNavigateToPreviousMessage = useCallback((): void => {
+ chat?.messageEditing.toPreviousMessage();
+ }, [chat?.messageEditing]);
+
+ const handleNavigateToNextMessage = useCallback((): void => {
+ chat?.messageEditing.toNextMessage();
+ }, [chat?.messageEditing]);
+
+ const handleUploadFiles = useCallback(
+ (files: readonly File[]): void => {
+ chat?.flows.uploadFiles(files);
+ },
+ [chat?.flows],
+ );
+
+ const room = useRoom();
+ 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]);
+
+ const jump = useQueryStringParameter('jump');
+
+ const [currentRouteName, currentRouteParams, currentRouteQueryStringParams] = useCurrentRoute();
+ if (!currentRouteName) {
+ throw new Error('No route name');
+ }
+ const currentRoute = useRoute(currentRouteName);
+
+ const handleJumpTo = useCallback(() => {
+ const newQueryStringParams = { ...currentRouteQueryStringParams };
+ delete newQueryStringParams.jump;
+ currentRoute.replace(currentRouteParams, newQueryStringParams);
+ }, [currentRoute, currentRouteParams, currentRouteQueryStringParams]);
+
+ const subscription = useRoomSubscription();
+ const sendToChannelID = useUniqueId();
+ const t = useTranslation();
+
+ const useLegacyMessageTemplate = useUserPreference('useLegacyMessageTemplate') ?? false;
+
+ return (
+
+
+
+ {useLegacyMessageTemplate ? (
+
+ ) : (
+ // TODO: create new thread message list
+
+ )}
+
+
+
+
+ );
+};
+
+export default ThreadChat;
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListItem.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListItem.tsx
new file mode 100644
index 000000000000..44432de2e84f
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListItem.tsx
@@ -0,0 +1,95 @@
+import type { IMessage } from '@rocket.chat/core-typings';
+import { css } from '@rocket.chat/css-in-js';
+import { Palette } from '@rocket.chat/fuselage';
+import { useMethod, useSetting, useToastMessageDispatch, useUserId } from '@rocket.chat/ui-contexts';
+import type { MouseEvent, ReactElement } from 'react';
+import React, { useCallback, memo } from 'react';
+
+import { useDecryptedMessage } from '../../../../../hooks/useDecryptedMessage';
+import { normalizeThreadMessage } from '../../../../../lib/normalizeThreadMessage';
+import ThreadListMessage from './ThreadListMessage';
+
+type ThreadListItemProps = {
+ thread: IMessage;
+ unread: string[];
+ unreadUser: string[];
+ unreadGroup: string[];
+ onClick: (tmid: IMessage['_id']) => void;
+};
+
+const ThreadListItem = ({ thread, unread, unreadUser, unreadGroup, onClick }: ThreadListItemProps): ReactElement => {
+ const uid = useUserId() ?? undefined;
+ const decryptedMsg = useDecryptedMessage(thread);
+ const msg = normalizeThreadMessage({ ...thread, msg: decryptedMsg });
+
+ const { name = thread.u.username } = thread.u;
+
+ const following = !!uid && (thread.replies?.includes(uid) ?? false);
+
+ const followMessage = useMethod('followMessage');
+ const unfollowMessage = useMethod('unfollowMessage');
+ const dispatchToastMessage = useToastMessageDispatch();
+
+ const toggleFollowMessage = useCallback(async (): Promise => {
+ try {
+ if (following) {
+ await unfollowMessage({ mid: thread._id });
+ } else {
+ await followMessage({ mid: thread._id });
+ }
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error });
+ }
+ }, [following, unfollowMessage, thread._id, followMessage, dispatchToastMessage]);
+
+ const handleToggleFollowButtonClick = useCallback(
+ (event: MouseEvent): void => {
+ event.preventDefault();
+ event.stopPropagation();
+ toggleFollowMessage();
+ },
+ [toggleFollowMessage],
+ );
+
+ const showRealNames = (useSetting('UI_Use_Real_Name') as boolean | undefined) ?? false;
+
+ const handleListItemClick = useCallback(
+ (event: MouseEvent): void => {
+ event.preventDefault();
+ event.stopPropagation();
+ onClick(thread._id);
+ },
+ [onClick, thread._id],
+ );
+
+ return (
+
+ );
+};
+
+export default memo(ThreadListItem);
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListMessage.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListMessage.tsx
index a0ca193f0e27..99128229424f 100644
--- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListMessage.tsx
+++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadListMessage.tsx
@@ -17,17 +17,16 @@ type ThreadListMessageProps = {
username: IMessage['u']['username'];
name?: IMessage['u']['name'];
ts: IMessage['ts'];
- replies: IMessage['replies'];
+ replies: ReactNode;
participants: ReactNode;
handleFollowButton: MouseEventHandler;
unread: boolean;
- mention: number;
+ mention: boolean;
all: boolean;
- tlm: number;
- className?: string | string[];
-} & Omit, 'className' | 'is'>;
+ tlm: Date | undefined;
+} & Omit, 'is'>;
-function ThreadListMessage({
+const ThreadListMessage = ({
_id,
msg,
following,
@@ -43,7 +42,7 @@ function ThreadListMessage({
tlm,
className = [],
...props
-}: ThreadListMessageProps): ReactElement {
+}: ThreadListMessageProps): ReactElement => {
const t = useTranslation();
const formatDate = useTimeAgo();
@@ -73,10 +72,12 @@ function ThreadListMessage({
{participants}
-
-
- {formatDate(tlm)}
-
+ {tlm && (
+
+
+ {formatDate(tlm)}
+
+ )}
@@ -99,6 +100,6 @@ function ThreadListMessage({
);
-}
+};
export default memo(ThreadListMessage);
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadSkeleton.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadSkeleton.tsx
new file mode 100644
index 000000000000..8517a46c8430
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadSkeleton.tsx
@@ -0,0 +1,18 @@
+import { Box, Skeleton } from '@rocket.chat/fuselage';
+import type { ReactElement } from 'react';
+import React from 'react';
+
+const ThreadSkeleton = (): ReactElement => {
+ return (
+
+
+ {Array(5)
+ .fill(5)
+ .map((_, index) => (
+
+ ))}
+
+ );
+};
+
+export default ThreadSkeleton;
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx
new file mode 100644
index 000000000000..c2044ba2afb6
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx
@@ -0,0 +1,16 @@
+import type { IThreadMainMessage } from '@rocket.chat/core-typings';
+import React, { useMemo } from 'react';
+
+import { normalizeThreadTitle } from '../../../../../../app/threads/client/lib/normalizeThreadTitle';
+import VerticalBar from '../../../../../components/VerticalBar';
+
+type ThreadTitleProps = {
+ mainMessage: IThreadMainMessage;
+};
+
+const ThreadTitle = ({ mainMessage }: ThreadTitleProps) => {
+ const innerHTML = useMemo(() => ({ __html: normalizeThreadTitle(mainMessage) }), [mainMessage]);
+ return ;
+};
+
+export default ThreadTitle;
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGetMessageByID.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGetMessageByID.ts
new file mode 100644
index 000000000000..e145d2c47e27
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGetMessageByID.ts
@@ -0,0 +1,28 @@
+import type { IMessage } from '@rocket.chat/core-typings';
+import { useEndpoint } from '@rocket.chat/ui-contexts';
+import { useCallback } from 'react';
+
+import { Messages } from '../../../../../../app/models/client';
+import { mapMessageFromApi } from '../../../../../lib/utils/mapMessageFromApi';
+
+export const useGetMessageByID = () => {
+ const getMessage = useEndpoint('GET', '/v1/chat.getMessage');
+
+ return useCallback(
+ async (mid: IMessage['_id']) => {
+ try {
+ const { message: rawMessage } = await getMessage({ msgId: mid });
+ const message = mapMessageFromApi(rawMessage);
+ Messages.upsert({ _id: message._id }, { $set: message });
+ return message;
+ } catch (error) {
+ if (typeof error === 'object' && error !== null && 'success' in error) {
+ throw new Error('Message not found');
+ }
+
+ throw error;
+ }
+ },
+ [getMessage],
+ );
+};
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGoToThread.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGoToThread.ts
new file mode 100644
index 000000000000..7901490efe7a
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGoToThread.ts
@@ -0,0 +1,22 @@
+import type { IMessage } from '@rocket.chat/core-typings';
+import { useCurrentRoute, useRoute } from '@rocket.chat/ui-contexts';
+import { useCallback } from 'react';
+
+import { useRoom } from '../../../contexts/RoomContext';
+
+export const useGoToThread = (): ((tmid: IMessage['_id']) => void) => {
+ const room = useRoom();
+ const [routeName] = useCurrentRoute();
+
+ if (!routeName) {
+ throw new Error('Route name is not defined');
+ }
+
+ const roomRoute = useRoute(routeName);
+ return useCallback(
+ (tmid) => {
+ roomRoute.replace({ rid: room._id, ...(room.name && { name: room.name }), tab: 'thread', context: tmid });
+ },
+ [room._id, room.name, roomRoute],
+ );
+};
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGoToThreadList.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGoToThreadList.ts
new file mode 100644
index 000000000000..a83f76bad3e6
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useGoToThreadList.ts
@@ -0,0 +1,18 @@
+import { useCurrentRoute, useRoute } from '@rocket.chat/ui-contexts';
+import { useCallback } from 'react';
+
+import { useRoom } from '../../../contexts/RoomContext';
+
+export const useGoToThreadList = (): (() => void) => {
+ const room = useRoom();
+ const [routeName] = useCurrentRoute();
+
+ if (!routeName) {
+ throw new Error('Route name is not defined');
+ }
+
+ const roomRoute = useRoute(routeName);
+ return useCallback(() => {
+ roomRoute.replace({ rid: room._id, ...(room.name && { name: room.name }), tab: 'thread' });
+ }, [room._id, room.name, roomRoute]);
+};
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts
new file mode 100644
index 000000000000..6d144f58793d
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts
@@ -0,0 +1,40 @@
+import type { IMessage } from '@rocket.chat/core-typings';
+import { useRef, useEffect } from 'react';
+
+export const useLegacyThreadMessageJump = (
+ mid: IMessage['_id'] | undefined,
+ { enabled = true, onJumpTo }: { enabled?: boolean; onJumpTo?: (mid: IMessage['_id']) => void },
+) => {
+ const parentRef = useRef(null);
+ const onJumpToRef = useRef(onJumpTo);
+ onJumpToRef.current = onJumpTo;
+
+ useEffect(() => {
+ const parent = parentRef.current;
+
+ if (!enabled || !mid || !parent) {
+ return;
+ }
+
+ const messageElement = parent.querySelector(`[data-id="${mid}"]`);
+ if (!messageElement) {
+ return;
+ }
+
+ messageElement.classList.add('highlight');
+
+ const removeClass = () => {
+ messageElement.classList.remove('highlight');
+ messageElement.removeEventListener('animationend', removeClass);
+ };
+ messageElement.addEventListener('animationend', removeClass);
+
+ setTimeout(() => {
+ messageElement.scrollIntoView();
+ const onJumpTo = onJumpToRef.current;
+ onJumpTo?.(mid);
+ }, 300);
+ }, [enabled, mid, onJumpTo]);
+
+ return { parentRef };
+};
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageListScrolling.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageListScrolling.ts
new file mode 100644
index 000000000000..b19f12c046d0
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageListScrolling.ts
@@ -0,0 +1,88 @@
+import type { UIEvent } from 'react';
+import { useContext, useCallback, useEffect, useRef } from 'react';
+
+import type { CommonRoomTemplateInstance } from '../../../../../../app/ui/client/views/app/lib/CommonRoomTemplateInstance';
+import { getCommonRoomEvents } from '../../../../../../app/ui/client/views/app/lib/getCommonRoomEvents';
+import { ChatContext } from '../../../contexts/ChatContext';
+import { useRoom } from '../../../contexts/RoomContext';
+import { useToolboxContext } from '../../../contexts/ToolboxContext';
+
+export const useLegacyThreadMessageListScrolling = () => {
+ const listWrapperRef = useRef(null);
+ const listRef = useRef(null);
+
+ const atBottomRef = useRef(true);
+
+ const onScroll = useCallback(({ currentTarget: e }: UIEvent) => {
+ atBottomRef.current = e.scrollTop >= e.scrollHeight - e.clientHeight;
+ }, []);
+
+ const sendToBottomIfNecessary = useCallback(() => {
+ if (atBottomRef.current === true) {
+ const listWrapper = listWrapperRef.current;
+
+ listWrapper?.scrollTo(30, listWrapper.scrollHeight);
+ }
+ }, []);
+
+ const toolbox = useToolboxContext();
+
+ const room = useRoom();
+ const chatContext = useContext(ChatContext);
+ useEffect(() => {
+ const messageList = listRef.current;
+
+ if (!messageList) {
+ return;
+ }
+
+ const messageEvents: Record void> = {
+ ...getCommonRoomEvents(),
+ '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 } }),
+ };
+ });
+
+ 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);
+ }
+ };
+ }, [chatContext, room._id, sendToBottomIfNecessary, toolbox]);
+
+ useEffect(() => {
+ const observer = new ResizeObserver(() => {
+ sendToBottomIfNecessary();
+ });
+
+ if (listWrapperRef.current) observer.observe(listWrapperRef.current);
+ if (listRef.current) observer.observe(listRef.current);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [sendToBottomIfNecessary]);
+
+ return { listWrapperRef, listRef, onScroll };
+};
diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageRef.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageRef.ts
new file mode 100644
index 000000000000..0a177116b843
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageRef.ts
@@ -0,0 +1,112 @@
+import type { IMessage } from '@rocket.chat/core-typings';
+import { isThreadMainMessage } from '@rocket.chat/core-typings';
+import { ReactiveVar } from 'meteor/reactive-var';
+import type { RefCallback } from 'react';
+import { useEffect, useMemo, useState, useContext, useCallback, useRef } from 'react';
+
+import MessageHighlightContext from '../../../MessageList/contexts/MessageHighlightContext';
+import { useRoomMessageContext } from '../../../components/body/useRoomMessageContext';
+import { ChatContext } from '../../../contexts/ChatContext';
+import { MessageContext } from '../../../contexts/MessageContext';
+import { useRoom } from '../../../contexts/RoomContext';
+
+export const useLegacyThreadMessageRef = () => {
+ const messageContext = useContext(MessageContext);
+ const chatContext = useContext(ChatContext);
+ const messageHighlightContext = useContext(MessageHighlightContext);
+ const room = useRoom();
+ const roomMessageContext = useRoomMessageContext(room);
+ const threadMessageContext = useMemo(
+ () => ({
+ ...roomMessageContext,
+ settings: {
+ ...roomMessageContext.settings,
+ showReplyButton: false,
+ showreply: false,
+ },
+ }),
+ [roomMessageContext],
+ );
+
+ const [reactiveThreadMessageContext] = useState(
+ () =>
+ new ReactiveVar({
+ ...threadMessageContext,
+ 'messageHighlightContext.highlightMessageId': messageHighlightContext.highlightMessageId,
+ messageContext,
+ chatContext,
+ }),
+ );
+ useEffect(() => {
+ reactiveThreadMessageContext.set({
+ ...threadMessageContext,
+ 'messageHighlightContext.highlightMessageId': messageHighlightContext.highlightMessageId,
+ messageContext,
+ chatContext,
+ });
+ }, [chatContext, messageContext, messageHighlightContext.highlightMessageId, reactiveThreadMessageContext, threadMessageContext]);
+
+ const cache = useRef