diff --git a/app/api/server/lib/messages.js b/app/api/server/lib/messages.js
index 18b0b71ca176e..257da349bb6ea 100644
--- a/app/api/server/lib/messages.js
+++ b/app/api/server/lib/messages.js
@@ -116,14 +116,14 @@ export async function findSnippetedMessages({ uid, roomId, pagination: { offset,
};
}
-export async function findDiscussionsFromRoom({ uid, roomId, pagination: { offset, count, sort } }) {
+export async function findDiscussionsFromRoom({ uid, roomId, text, pagination: { offset, count, sort } }) {
const room = await Rooms.findOneById(roomId);
if (!await canAccessRoomAsync(room, { _id: uid })) {
throw new Error('error-not-allowed');
}
- const cursor = Messages.findDiscussionsByRoom(roomId, {
+ const cursor = Messages.findDiscussionsByRoomAndText(roomId, text, {
sort: sort || { ts: -1 },
skip: offset,
limit: count,
diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js
index 6c77dd068317e..b10a66c4e9332 100644
--- a/app/api/server/v1/chat.js
+++ b/app/api/server/v1/chat.js
@@ -697,7 +697,7 @@ API.v1.addRoute('chat.getSnippetedMessages', { authRequired: true }, {
API.v1.addRoute('chat.getDiscussions', { authRequired: true }, {
get() {
- const { roomId } = this.queryParams;
+ const { roomId, text } = this.queryParams;
const { sort } = this.parseJsonQuery();
const { offset, count } = this.getPaginationItems();
@@ -707,6 +707,7 @@ API.v1.addRoute('chat.getDiscussions', { authRequired: true }, {
const messages = Promise.await(findDiscussionsFromRoom({
uid: this.userId,
roomId,
+ text,
pagination: {
offset,
count,
diff --git a/app/discussion/client/tabBar.js b/app/discussion/client/tabBar.js
index 661c9a8ddebf2..5b5873d2a41b8 100644
--- a/app/discussion/client/tabBar.js
+++ b/app/discussion/client/tabBar.js
@@ -10,6 +10,7 @@ Meteor.startup(function() {
i18nTitle: 'Discussions',
icon: 'discussion',
template: 'discussionsTabbar',
+ full: true,
order: 1,
condition: () => settings.get('Discussion_enabled'),
});
diff --git a/app/discussion/client/views/DiscussionTabbar.html b/app/discussion/client/views/DiscussionTabbar.html
index b489dcedb79f3..80f65ec9d16c1 100644
--- a/app/discussion/client/views/DiscussionTabbar.html
+++ b/app/discussion/client/views/DiscussionTabbar.html
@@ -1,24 +1,3 @@
- {{#if Template.subscriptionsReady}}
- {{#unless hasMessages}}
-
- {{/unless}}
- {{/if}}
-
-
- {{# with messageContext}}
- {{#each msg in messages}}
- {{> message msg=msg room=room subscription=subscription groupable=false settings=settings u=u}}
- {{/each}}
- {{/with}}
-
-
- {{#if hasMore}}
-
- {{> loading}}
-
- {{/if}}
-
+ {{ > DiscussionMessageList rid=rid onClose=close}}
diff --git a/app/discussion/client/views/DiscussionTabbar.js b/app/discussion/client/views/DiscussionTabbar.js
index 956428e899f10..acbae6d694bb5 100644
--- a/app/discussion/client/views/DiscussionTabbar.js
+++ b/app/discussion/client/views/DiscussionTabbar.js
@@ -1,74 +1,11 @@
-import _ from 'underscore';
-import { ReactiveVar } from 'meteor/reactive-var';
-import { Mongo } from 'meteor/mongo';
import { Template } from 'meteor/templating';
-import { messageContext } from '../../../ui-utils/client/lib/messageContext';
-import { Messages } from '../../../models/client';
-import { APIClient } from '../../../utils/client';
-import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager';
-
import './DiscussionTabbar.html';
-const LIMIT_DEFAULT = 50;
-
Template.discussionsTabbar.helpers({
- hasMessages() {
- return Template.instance().messages.find().count();
- },
- messages() {
- const instance = Template.instance();
- return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } });
- },
- hasMore() {
- return Template.instance().hasMore.get();
+ close() {
+ const { data } = Template.instance();
+ const { tabBar } = data;
+ return () => tabBar.close();
},
- messageContext,
-});
-
-Template.discussionsTabbar.onCreated(function() {
- this.rid = this.data.rid;
- this.messages = new Mongo.Collection(null);
- this.hasMore = new ReactiveVar(true);
- this.limit = new ReactiveVar(LIMIT_DEFAULT);
-
- this.autorun(() => {
- const query = {
- rid: this.rid,
- drid: { $exists: true },
- };
-
- this.cursor && this.cursor.stop();
-
- this.limit.set(LIMIT_DEFAULT);
-
- this.cursor = Messages.find(query).observe({
- added: ({ _id, ...message }) => {
- this.messages.upsert({ _id }, message);
- },
- changed: ({ _id, ...message }) => {
- this.messages.upsert({ _id }, message);
- },
- removed: ({ _id }) => {
- this.messages.remove({ _id });
- },
- });
- });
-
- this.autorun(async () => {
- const limit = this.limit.get();
- const { messages, total } = await APIClient.v1.get(`chat.getDiscussions?roomId=${ this.rid }&count=${ limit }`);
-
- upsertMessageBulk({ msgs: messages }, this.messages);
-
- this.hasMore.set(total > limit);
- });
-});
-
-Template.discussionsTabbar.events({
- 'scroll .js-list': _.throttle(function(e, instance) {
- if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight - 10 && instance.hasMore.get()) {
- instance.limit.set(instance.limit.get() + LIMIT_DEFAULT);
- }
- }, 200),
});
diff --git a/app/models/server/raw/Messages.js b/app/models/server/raw/Messages.js
index 6326293c4142e..ad09996f7f464 100644
--- a/app/models/server/raw/Messages.js
+++ b/app/models/server/raw/Messages.js
@@ -48,6 +48,20 @@ export class MessagesRaw extends BaseRaw {
return this.find(query, options);
}
+ findDiscussionsByRoomAndText(rid, text, options) {
+ const query = {
+ rid,
+ drid: { $exists: true },
+ ...text && {
+ $text: {
+ $search: text,
+ },
+ },
+ };
+
+ return this.find(query, options);
+ }
+
findAllNumberOfTransferredRooms({ start, end, departmentId, onlyCount = false, options = {} }) {
const match = {
$match: {
diff --git a/app/threads/client/components/ThreadComponent.js b/app/threads/client/components/ThreadComponent.js
index 20d2881e4274b..41dec230912a1 100644
--- a/app/threads/client/components/ThreadComponent.js
+++ b/app/threads/client/components/ThreadComponent.js
@@ -11,7 +11,7 @@ import { roomTypes, APIClient } from '../../../utils/client';
import { call } from '../../../ui-utils/client';
import { useTranslation } from '../../../../client/contexts/TranslationContext';
import VerticalBar from '../../../../client/components/basic/VerticalBar';
-import { useLocalStorage } from './hooks/useLocalstorage';
+import { useLocalStorage } from '../../../../client/Channel/hooks/useLocalstorage';
import { normalizeThreadTitle } from '../lib/normalizeThreadTitle';
export default function ThreadComponent({ mid, rid, jump, room, ...props }) {
diff --git a/app/threads/client/components/ThreadListMessage.js b/app/threads/client/components/ThreadListMessage.js
deleted file mode 100644
index 4fe07ce40113e..0000000000000
--- a/app/threads/client/components/ThreadListMessage.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import React from 'react';
-import { Box, Margins, Button, Icon, Skeleton } from '@rocket.chat/fuselage';
-import { css } from '@rocket.chat/css-in-js';
-
-import UserAvatar from '../../../../client/components/basic/avatar/UserAvatar';
-import RawText from '../../../../client/components/basic/RawText';
-
-const borderRadius = css`
- border-radius: 100%;
-`;
-
-export function NotificationStatus({ t = (e) => e, label, ...props }) {
- return ;
-}
-
-export function NotificationStatusAll(props) {
- return ;
-}
-
-export function NotificationStatusMe(props) {
- return ;
-}
-
-export function NotificationStatusUnread(props) {
- return ;
-}
-
-function isIterable(obj) {
- // checks for null and undefined
- if (obj == null) {
- return false;
- }
- return typeof obj[Symbol.iterator] === 'function';
-}
-
-const followStyle = css`
- & > .rcx-message__container > .rcx-contextual-message__follow {
- opacity: 0;
- }
- .rcx-contextual-message__follow:focus,
- &:hover > .rcx-message__container > .rcx-contextual-message__follow,
- &:focus > .rcx-message__container > .rcx-contextual-message__follow {
- opacity: 1
- }
-`;
-
-export default function ThreadListMessage({ _id, msg, following, username, name, ts, replies, participants, handleFollowButton, unread, mention, all, t = (e) => e, formatDate = (e) => e, tlm, className = [], ...props }) {
- const button = !following ? 'bell-off' : 'bell';
- const actionLabel = t(!following ? 'Not_Following' : 'Following');
-
- return
-
-
-
-
-
- {msg}
-
-
- {replies}
- {participants}
- {formatDate(tlm)}
-
-
-
-
-
- {
- (mention && )
- || (all && )
- || (unread && )
- }
-
- ;
-}
-
-export function MessageSkeleton(props) {
- return
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ;
-}
-
-function Container({ children, ...props }) {
- return {children};
-}
-
-function Header({ children }) {
- return {children} ;
-}
-
-function Username(props) {
- return ;
-}
-
-function Timestamp({ ts }) {
- return {ts.toDateString ? ts.toDateString() : ts };
-}
-
-const style = {
- display: '-webkit-box',
- overflow: 'hidden',
- WebkitLineClamp: 2,
- WebkitBoxOrient: 'vertical',
- wordBreak: 'break-all',
-};
-
-function Body(props) {
- return ;
-}
diff --git a/app/threads/client/components/hooks/useUserRoom.js b/app/threads/client/components/hooks/useUserRoom.js
deleted file mode 100644
index b4d858e54cb36..0000000000000
--- a/app/threads/client/components/hooks/useUserRoom.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { useCallback } from 'react';
-
-import { useReactiveValue } from '../../../../../client/hooks/useReactiveValue';
-import { Rooms } from '../../../../models/client';
-
-export const useUserRoom = (rid, fields) =>
- useReactiveValue(useCallback(() => Rooms.findOne({ _id: rid }, { fields }), [rid, fields]));
diff --git a/app/threads/client/components/hooks/useUserSubscription.js b/app/threads/client/components/hooks/useUserSubscription.js
deleted file mode 100644
index 4bcf81e79a66f..0000000000000
--- a/app/threads/client/components/hooks/useUserSubscription.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { useCallback } from 'react';
-
-import { useReactiveValue } from '../../../../../client/hooks/useReactiveValue';
-import { Subscriptions } from '../../../../models/client';
-
-export const useUserSubscription = (rid, fields) =>
- useReactiveValue(useCallback(() => Subscriptions.findOne({ rid }, { fields }), [rid, fields]));
diff --git a/app/threads/client/flextab/threads.js b/app/threads/client/flextab/threads.js
index f5bd3f50ffe2d..c302a5f00ada6 100644
--- a/app/threads/client/flextab/threads.js
+++ b/app/threads/client/flextab/threads.js
@@ -1,15 +1,8 @@
import { Template } from 'meteor/templating';
-import { HTML } from 'meteor/htmljs';
import './threads.html';
import '../threads.css';
-import { createTemplateForComponent } from '../../../../client/reactAdapters';
-
-
-createTemplateForComponent('ThreadsList', () => import('../components/ThreadList'), {
- renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
-});
Template.threads.helpers({
rid() {
diff --git a/client/Channel/Discussions/ContextualBar/List.js b/client/Channel/Discussions/ContextualBar/List.js
new file mode 100644
index 0000000000000..c2f8fa3a421d4
--- /dev/null
+++ b/client/Channel/Discussions/ContextualBar/List.js
@@ -0,0 +1,223 @@
+import { Mongo } from 'meteor/mongo';
+import { Tracker } from 'meteor/tracker';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import s from 'underscore.string';
+import React, { useCallback, useMemo, useState, useEffect, useRef } from 'react';
+import { Box, Icon, TextInput, Callout } from '@rocket.chat/fuselage';
+import { FixedSizeList as List } from 'react-window';
+import InfiniteLoader from 'react-window-infinite-loader';
+import { useDebouncedValue, useDebouncedState, useResizeObserver } from '@rocket.chat/fuselage-hooks';
+
+import { renderMessageBody } from '../../../../app/ui-utils/client';
+import { getConfig } from '../../../../app/ui-utils/client/config';
+import { Messages } from '../../../../app/models/client';
+import VerticalBar from '../../../components/basic/VerticalBar';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import RawText from '../../../components/basic/RawText';
+import { useUserId } from '../../../contexts/UserContext';
+import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental';
+import { useTimeAgo } from '../../../hooks/useTimeAgo';
+import { MessageSkeleton } from '../../components/Message';
+import { useUserSubscription } from '../../hooks/useUserSubscription';
+import { useUserRoom } from '../../hooks/useUserRoom';
+import { useSetting } from '../../../contexts/SettingsContext';
+import DiscussionListMessage from './components/Message';
+import { clickableItem } from '../../helpers/clickableItem';
+
+function mapProps(WrappedComponent) {
+ return ({ msg, username, tcount, ts, ...props }) => ;
+}
+
+const Discussion = React.memo(mapProps(clickableItem(DiscussionListMessage)));
+
+const Skeleton = React.memo(clickableItem(MessageSkeleton));
+
+const LIST_SIZE = parseInt(getConfig('discussionListSize')) || 25;
+
+const filterProps = ({ msg, drid, u, dcount, mentions, tcount, ts, _id, dlm, attachments, name }) => ({ ..._id && { _id }, drid, attachments, name, mentions, msg, u, dcount, tcount, ts: new Date(ts), dlm: new Date(dlm) });
+
+const subscriptionFields = { tunread: 1, tunreadUser: 1, tunreadGroup: 1 };
+const roomFields = { t: 1, name: 1 };
+
+export function withData(WrappedComponent) {
+ return ({ rid, ...props }) => {
+ const room = useUserRoom(rid, roomFields);
+ const subscription = useUserSubscription(rid, subscriptionFields);
+ const userId = useUserId();
+
+ const [text, setText] = useState('');
+ const [total, setTotal] = useState(LIST_SIZE);
+ const [discussions, setDiscussions] = useDebouncedState([], 100);
+ const Discussions = useRef(new Mongo.Collection(null));
+ const ref = useRef();
+ const [pagination, setPagination] = useState({ skip: 0, count: LIST_SIZE });
+
+ const params = useMemo(() => ({ roomId: room._id, count: pagination.count, offset: pagination.skip, text }), [room._id, pagination.skip, pagination.count, text]);
+
+ const { data, state, error } = useEndpointDataExperimental('chat.getDiscussions', useDebouncedValue(params, 400));
+
+ const loadMoreItems = useCallback((skip, count) => {
+ setPagination({ skip, count: count - skip });
+
+ return new Promise((resolve) => { ref.current = resolve; });
+ }, []);
+
+ useEffect(() => () => Discussions.current.remove({}, () => {}), [text]);
+
+ useEffect(() => {
+ if (state !== ENDPOINT_STATES.DONE || !data || !data.messages) {
+ return;
+ }
+
+ data.messages.forEach(({ _id, ...message }) => {
+ Discussions.current.upsert({ _id }, filterProps(message));
+ });
+
+ setTotal(data.total);
+ ref.current && ref.current();
+ }, [data, state]);
+
+ useEffect(() => {
+ const cursor = Messages.find({ rid: room._id, drid: { $exists: true } }).observe({
+ added: ({ _id, ...message }) => {
+ Discussions.current.upsert({ _id }, message);
+ }, // Update message to re-render DOM
+ changed: ({ _id, ...message }) => {
+ Discussions.current.update({ _id }, message);
+ }, // Update message to re-render DOM
+ removed: ({ _id }) => {
+ Discussions.current.remove(_id);
+ },
+ });
+ return () => cursor.stop();
+ }, [room._id]);
+
+
+ useEffect(() => {
+ const cursor = Tracker.autorun(() => {
+ const query = {
+ };
+ setDiscussions(Discussions.current.find(query, { sort: { tlm: -1 } }).fetch().map(filterProps));
+ });
+
+ return () => cursor.stop();
+ }, [room._id, setDiscussions, userId]);
+
+ const handleTextChange = useCallback((e) => {
+ setPagination({ skip: 0, count: LIST_SIZE });
+ setText(e.currentTarget.value);
+ }, []);
+
+ return ;
+ };
+}
+
+export const normalizeThreadMessage = ({ ...message }) => {
+ if (message.msg) {
+ return renderMessageBody(message).replace(/
/g, ' ');
+ }
+
+ if (message.attachments) {
+ const attachment = message.attachments.find((attachment) => attachment.title || attachment.description);
+
+ if (attachment && attachment.description) {
+ return s.escapeHTML(attachment.description);
+ }
+
+ if (attachment && attachment.title) {
+ return s.escapeHTML(attachment.title);
+ }
+ }
+};
+
+export function DiscussionList({ total = 10, discussions = [], loadMoreItems, loading, onClose, error, userId, text, setText }) {
+ const showRealNames = useSetting('UI_Use_Real_Name');
+ const discussionsRef = useRef();
+
+ const t = useTranslation();
+
+ const onClick = useCallback((e) => {
+ const { drid } = e.currentTarget.dataset;
+ FlowRouter.goToRoomById(drid);
+ }, []);
+
+ const formatDate = useTimeAgo();
+
+ discussionsRef.current = discussions;
+
+ const rowRenderer = useCallback(React.memo(function rowRenderer({ data, index, style }) {
+ if (!data[index]) {
+ return ;
+ }
+ const discussion = data[index];
+ const msg = normalizeThreadMessage(discussion);
+
+ const { name = discussion.u.username } = discussion.u;
+
+ return ;
+ }), [showRealNames]);
+
+ const isItemLoaded = useCallback((index) => index < discussionsRef.current.length, []);
+ const { ref, contentBoxSize: { inlineSize = 378, blockSize = 750 } = {} } = useResizeObserver();
+
+ return
+
+
+ {t('Discussions')}
+
+
+
+
+ }/>
+
+
+ {error && {error.toString()}}
+ {total === 0 && {t('No_Discussions_found')}}
+ {} : loadMoreItems}
+ >
+ {({ onItemsRendered, ref }) => ({rowRenderer}
+ )}
+
+
+
+ ;
+}
+
+export default withData(DiscussionList);
diff --git a/client/Channel/Discussions/ContextualBar/components/Message.js b/client/Channel/Discussions/ContextualBar/components/Message.js
new file mode 100644
index 0000000000000..284f77d87cc58
--- /dev/null
+++ b/client/Channel/Discussions/ContextualBar/components/Message.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Box, Icon } from '@rocket.chat/fuselage';
+
+import UserAvatar from '../../../../components/basic/avatar/UserAvatar';
+import RawText from '../../../../components/basic/RawText';
+import * as MessageTemplate from '../../../components/Message';
+
+
+export default React.memo(function Message({ _id, msg, following, username, name = username, ts, dcount, t = (text) => text, participants, handleFollowButton, unread, mention, all, formatDate = (e) => e, dlm, className = [], ...props }) {
+ return
+
+
+
+
+
+ {name}
+
+
+ {msg}
+
+ {!dcount && {t('No_messages_yet')}}
+ { !!dcount && {dcount}}
+ { !!dcount && {formatDate(dlm)} }
+
+
+ ;
+});
diff --git a/client/Channel/Discussions/ContextualBar/components/Message.stories.js b/client/Channel/Discussions/ContextualBar/components/Message.stories.js
new file mode 100644
index 0000000000000..b051d6c515e9f
--- /dev/null
+++ b/client/Channel/Discussions/ContextualBar/components/Message.stories.js
@@ -0,0 +1,32 @@
+import React from 'react';
+
+import Message from './Message';
+
+const message = {
+ msg: 'hello world',
+ ts: new Date(0),
+ username: 'guilherme.gazzo',
+ dcount: 5,
+ dlm: new Date(0).toISOString(),
+};
+
+const largeText = {
+ ...message,
+ msg: 'Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text',
+};
+
+const noReplies = {
+ ...message,
+ dcount: 0,
+};
+
+export default {
+ title: 'components/Discussion/Message',
+ component: Message,
+};
+
+export const Basic = () => ;
+
+export const LargeText = () => ;
+
+export const NoReplies = () => ;
diff --git a/app/threads/client/components/ThreadList.js b/client/Channel/Threads/ContextualBar/List.js
similarity index 85%
rename from app/threads/client/components/ThreadList.js
rename to client/Channel/Threads/ContextualBar/List.js
index 13ea603df54ef..996a4e9bbe2fc 100644
--- a/app/threads/client/components/ThreadList.js
+++ b/client/Channel/Threads/ContextualBar/List.js
@@ -6,38 +6,25 @@ import { Box, Icon, TextInput, Select, Margins, Callout } from '@rocket.chat/fus
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { useDebouncedValue, useDebouncedState, useResizeObserver } from '@rocket.chat/fuselage-hooks';
-import { css } from '@rocket.chat/css-in-js';
-
-import VerticalBar from '../../../../client/components/basic/VerticalBar';
-import { useTranslation } from '../../../../client/contexts/TranslationContext';
-import RawText from '../../../../client/components/basic/RawText';
-import { useRoute } from '../../../../client/contexts/RouterContext';
-import { roomTypes } from '../../../utils/client';
-import { call, renderMessageBody } from '../../../ui-utils/client';
-import { useUserId } from '../../../../client/contexts/UserContext';
-import { Messages } from '../../../models/client';
-import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../../client/hooks/useEndpointDataExperimental';
-import { getConfig } from '../../../ui-utils/client/config';
-import { useTimeAgo } from '../../../../client/hooks/useTimeAgo';
-import ThreadListMessage, { MessageSkeleton } from './ThreadListMessage';
-import { useUserSubscription } from './hooks/useUserSubscription';
-import { useUserRoom } from './hooks/useUserRoom';
-import { useLocalStorage } from './hooks/useLocalstorage';
-import { useSetting } from '../../../../client/contexts/SettingsContext';
-
-
-function clickableItem(WrappedComponent) {
- const clickable = css`
- cursor: pointer;
- border-bottom: 2px solid #F2F3F5 !important;
-
- &:hover,
- &:focus {
- background: #F7F8FA;
- }
- `;
- return (props) => ;
-}
+
+import { roomTypes } from '../../../../app/utils/client';
+import { call, renderMessageBody } from '../../../../app/ui-utils/client';
+import { getConfig } from '../../../../app/ui-utils/client/config';
+import { Messages } from '../../../../app/models/client';
+import VerticalBar from '../../../components/basic/VerticalBar';
+import { useTranslation } from '../../../contexts/TranslationContext';
+import RawText from '../../../components/basic/RawText';
+import { useRoute } from '../../../contexts/RouterContext';
+import { useUserId } from '../../../contexts/UserContext';
+import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental';
+import { useTimeAgo } from '../../../hooks/useTimeAgo';
+import { MessageSkeleton } from '../../components/Message';
+import { useUserSubscription } from '../../hooks/useUserSubscription';
+import { useUserRoom } from '../../hooks/useUserRoom';
+import { useLocalStorage } from '../../hooks/useLocalstorage';
+import { useSetting } from '../../../contexts/SettingsContext';
+import ThreadListMessage from './components/Message';
+import { clickableItem } from '../../helpers/clickableItem';
function mapProps(WrappedComponent) {
return ({ msg, username, replies, tcount, ts, ...props }) => ;
diff --git a/client/Channel/Threads/ContextualBar/components/Message.js b/client/Channel/Threads/ContextualBar/components/Message.js
new file mode 100644
index 0000000000000..d80d4babf610a
--- /dev/null
+++ b/client/Channel/Threads/ContextualBar/components/Message.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Box, Button, Icon } from '@rocket.chat/fuselage';
+import { css } from '@rocket.chat/css-in-js';
+
+import UserAvatar from '../../../../components/basic/avatar/UserAvatar';
+import RawText from '../../../../components/basic/RawText';
+import * as MessageTemplate from '../../../components/Message';
+import * as NotificationStatus from '../../../components/NotificationStatus';
+
+function isIterable(obj) {
+ // checks for null and undefined
+ if (obj == null) {
+ return false;
+ }
+ return typeof obj[Symbol.iterator] === 'function';
+}
+
+const followStyle = css`
+ & > .rcx-message__container > .rcx-contextual-message__follow {
+ opacity: 0;
+ }
+ .rcx-contextual-message__follow:focus,
+ &:hover > .rcx-message__container > .rcx-contextual-message__follow,
+ &:focus > .rcx-message__container > .rcx-contextual-message__follow {
+ opacity: 1;
+ }
+`;
+
+export default React.memo(function Message({ _id, msg, following, username, name = username, ts, replies, participants, handleFollowButton, unread, mention, all, t = (e) => e, formatDate = (e) => e, tlm, className = [], ...props }) {
+ const button = !following ? 'bell-off' : 'bell';
+ const actionLabel = t(!following ? 'Not_Following' : 'Following');
+
+ return
+
+
+
+
+
+ {name}
+
+
+ {msg}
+
+ {replies}
+ {participants}
+ {formatDate(tlm)}
+
+
+
+
+ {
+ (mention && )
+ || (all && )
+ || (unread && )
+ }
+
+ ;
+});
diff --git a/client/Channel/Threads/ContextualBar/components/Message.stories.js b/client/Channel/Threads/ContextualBar/components/Message.stories.js
new file mode 100644
index 0000000000000..ecad030a1733d
--- /dev/null
+++ b/client/Channel/Threads/ContextualBar/components/Message.stories.js
@@ -0,0 +1,55 @@
+import React from 'react';
+
+import Message from './Message';
+
+const message = {
+ msg: 'hello world',
+ ts: new Date(0),
+ username: 'guilherme.gazzo',
+ replies: 1,
+ participants: 2,
+ tlm: new Date(0).toISOString(),
+};
+
+const largeText = {
+ ...message,
+ msg: 'Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text, Large text',
+};
+
+const following = {
+ ...largeText,
+ following: true,
+};
+
+
+const unread = {
+ ...largeText,
+ unread: true,
+};
+
+const all = {
+ ...unread,
+ all: true,
+};
+
+const mention = {
+ ...all,
+ mention: true,
+};
+
+export default {
+ title: 'components/Threads/Message',
+ component: Message,
+};
+
+export const Basic = () => ;
+
+export const LargeText = () => ;
+
+export const Following = () => ;
+
+export const Unread = () => ;
+
+export const Mention = () => ;
+
+export const MentionAll = () => ;
diff --git a/client/Channel/adapters.js b/client/Channel/adapters.js
new file mode 100644
index 0000000000000..1395da047ee8e
--- /dev/null
+++ b/client/Channel/adapters.js
@@ -0,0 +1,11 @@
+import { HTML } from 'meteor/htmljs';
+
+import { createTemplateForComponent } from '../reactAdapters';
+
+createTemplateForComponent('DiscussionMessageList', () => import('./Discussions/ContextualBar/List'), {
+ renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
+});
+
+createTemplateForComponent('ThreadsList', () => import('./Threads/ContextualBar/List'), {
+ renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap
+});
diff --git a/client/Channel/components/Message.js b/client/Channel/components/Message.js
new file mode 100644
index 0000000000000..5d28d3ee9e69e
--- /dev/null
+++ b/client/Channel/components/Message.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import { Box, Margins, Skeleton } from '@rocket.chat/fuselage';
+import { css } from '@rocket.chat/css-in-js';
+
+export const MessageSkeleton = React.memo(function MessageSkeleton(props) {
+ return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;
+});
+
+export function Container({ children, ...props }) {
+ return {children};
+}
+
+export function Header({ children }) {
+ return {children} ;
+}
+
+export function Username(props) {
+ return ;
+}
+
+export function Timestamp({ ts }) {
+ return {ts.toDateString ? ts.toDateString() : ts };
+}
+
+function isIterable(obj) {
+ // checks for null and undefined
+ if (obj == null) {
+ return false;
+ }
+ return typeof obj[Symbol.iterator] === 'function';
+}
+
+export function Message({ className, ...props }) {
+ return ;
+}
+
+export default Message;
+
+const style = css`
+ display: -webkit-box;
+ overflow: hidden;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ word-break: break-word;
+`;
+
+export function BodyClamp(props) {
+ return ;
+}
diff --git a/client/Channel/components/NotificationStatus.js b/client/Channel/components/NotificationStatus.js
new file mode 100644
index 0000000000000..c53990aee982b
--- /dev/null
+++ b/client/Channel/components/NotificationStatus.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Box } from '@rocket.chat/fuselage';
+
+export function NotificationStatus({ t = (e) => e, label, ...props }) {
+ return ;
+}
+
+export function All(props) {
+ return ;
+}
+
+export function Me(props) {
+ return ;
+}
+
+export function Unread(props) {
+ return ;
+}
diff --git a/client/Channel/helpers/clickableItem.js b/client/Channel/helpers/clickableItem.js
new file mode 100644
index 0000000000000..6992dfcefd267
--- /dev/null
+++ b/client/Channel/helpers/clickableItem.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import { css } from '@rocket.chat/css-in-js';
+
+export function clickableItem(WrappedComponent) {
+ const clickable = css`
+ cursor: pointer;
+ border-bottom: 2px solid #F2F3F5 !important;
+
+ &:hover,
+ &:focus {
+ background: #F7F8FA;
+ }
+ `;
+ return (props) => ;
+}
diff --git a/app/threads/client/components/hooks/useLocalstorage.js b/client/Channel/hooks/useLocalstorage.js
similarity index 100%
rename from app/threads/client/components/hooks/useLocalstorage.js
rename to client/Channel/hooks/useLocalstorage.js
diff --git a/client/Channel/hooks/useUserRoom.js b/client/Channel/hooks/useUserRoom.js
new file mode 100644
index 0000000000000..e981174da5a56
--- /dev/null
+++ b/client/Channel/hooks/useUserRoom.js
@@ -0,0 +1,6 @@
+import { useCallback } from 'react';
+
+import { useReactiveValue } from '../../hooks/useReactiveValue';
+import { Rooms } from '../../../app/models/client';
+
+export const useUserRoom = (rid, fields) => useReactiveValue(useCallback(() => Rooms.findOne({ _id: rid }, { fields }), [rid, fields]));
diff --git a/client/Channel/hooks/useUserSubscription.js b/client/Channel/hooks/useUserSubscription.js
new file mode 100644
index 0000000000000..3d9d96e03566e
--- /dev/null
+++ b/client/Channel/hooks/useUserSubscription.js
@@ -0,0 +1,6 @@
+import { useCallback } from 'react';
+
+import { useReactiveValue } from '../../hooks/useReactiveValue';
+import { Subscriptions } from '../../../app/models/client';
+
+export const useUserSubscription = (rid, fields) => useReactiveValue(useCallback(() => Subscriptions.findOne({ rid }, { fields }), [rid, fields]));
diff --git a/client/Channel/index.js b/client/Channel/index.js
new file mode 100644
index 0000000000000..52309a47b91a2
--- /dev/null
+++ b/client/Channel/index.js
@@ -0,0 +1 @@
+import './adapters';
diff --git a/client/main.js b/client/main.js
index b8edae8a3f305..fc98e66348cf0 100644
--- a/client/main.js
+++ b/client/main.js
@@ -30,3 +30,4 @@ import './startup/unread';
import './startup/userSetUtcOffset';
import './startup/usersObserve';
import './admin';
+import './Channel';
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index f7f93ee7cca61..e42f1ea0f9c67 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -2611,6 +2611,7 @@
"No_starred_messages": "No starred messages",
"No_such_command": "No such command: `/__command__`",
"No_discussions_yet": "No discussions yet",
+ "No_Discussions_found": "No discussions found",
"No_Threads": "No threads found",
"No_user_with_username_%s_was_found": "No user with username \"%s\" was found!",
"No_data_found": "No data found",