Rewrite Contextual Bar Discussion List in React (#18127)

pull/18190/head^2
Guilherme Gazzo 5 years ago committed by GitHub
parent b711b12331
commit c492c61c9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/api/server/lib/messages.js
  2. 3
      app/api/server/v1/chat.js
  3. 1
      app/discussion/client/tabBar.js
  4. 23
      app/discussion/client/views/DiscussionTabbar.html
  5. 71
      app/discussion/client/views/DiscussionTabbar.js
  6. 14
      app/models/server/raw/Messages.js
  7. 2
      app/threads/client/components/ThreadComponent.js
  8. 127
      app/threads/client/components/ThreadListMessage.js
  9. 7
      app/threads/client/components/hooks/useUserRoom.js
  10. 7
      app/threads/client/components/hooks/useUserSubscription.js
  11. 7
      app/threads/client/flextab/threads.js
  12. 223
      client/Channel/Discussions/ContextualBar/List.js
  13. 27
      client/Channel/Discussions/ContextualBar/components/Message.js
  14. 32
      client/Channel/Discussions/ContextualBar/components/Message.stories.js
  15. 51
      client/Channel/Threads/ContextualBar/List.js
  16. 58
      client/Channel/Threads/ContextualBar/components/Message.js
  17. 55
      client/Channel/Threads/ContextualBar/components/Message.stories.js
  18. 11
      client/Channel/adapters.js
  19. 66
      client/Channel/components/Message.js
  20. 18
      client/Channel/components/NotificationStatus.js
  21. 15
      client/Channel/helpers/clickableItem.js
  22. 0
      client/Channel/hooks/useLocalstorage.js
  23. 6
      client/Channel/hooks/useUserRoom.js
  24. 6
      client/Channel/hooks/useUserSubscription.js
  25. 1
      client/Channel/index.js
  26. 1
      client/main.js
  27. 1
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -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,

@ -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,

@ -10,6 +10,7 @@ Meteor.startup(function() {
i18nTitle: 'Discussions',
icon: 'discussion',
template: 'discussionsTabbar',
full: true,
order: 1,
condition: () => settings.get('Discussion_enabled'),
});

@ -1,24 +1,3 @@
<template name="discussionsTabbar">
{{#if Template.subscriptionsReady}}
{{#unless hasMessages}}
<div class="list-view discussions-list flex-tab__header">
<h2>{{_ "No_discussions_yet"}}</h2>
</div>
{{/unless}}
{{/if}}
<div class="flex-tab__result discussions-list js-list">
<ul class="list clearfix">
{{# with messageContext}}
{{#each msg in messages}}
{{> message msg=msg room=room subscription=subscription groupable=false settings=settings u=u}}
{{/each}}
{{/with}}
</ul>
{{#if hasMore}}
<div class="load-more">
{{> loading}}
</div>
{{/if}}
</div>
{{ > DiscussionMessageList rid=rid onClose=close}}
</template>

@ -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),
});

@ -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: {

@ -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 }) {

@ -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 <Box width='x8' aria-label={t(label)} className={[borderRadius]} height='x8' {...props} />;
}
export function NotificationStatusAll(props) {
return <NotificationStatus label='mention-all' bg='#F38C39' {...props} />;
}
export function NotificationStatusMe(props) {
return <NotificationStatus label='Me' bg='danger-500' {...props} />;
}
export function NotificationStatusUnread(props) {
return <NotificationStatus label='Unread' bg='primary-500' {...props} />;
}
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 <Box rcx-contextual-message pi='x20' pb='x16' pbs='x16' display='flex' {...props} className={[...isIterable(className) ? className : [className], !following && followStyle].filter(Boolean)}>
<Container mb='neg-x2'>
<UserAvatar username={username} rcx-message__avatar size='x36'/>
</Container>
<Container width='1px' mb='neg-x4' flexGrow={1}>
<Header>
<Username title={username}>{name}</Username>
<Timestamp ts={formatDate(ts)}/>
</Header>
<Body><RawText>{msg}</RawText></Body>
<Box mi='neg-x2' flexDirection='row' display='flex' alignItems='baseline' mbs='x8'>
<Margins inline='x2'>
<Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600'><Icon name='thread' size='x20' mi='x2'/> {replies} </Box>
<Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600'><Icon name='user' size='x20' mi='x2'/> {participants} </Box>
<Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600' withTruncatedText flexShrink={1}><Icon name='clock' size='x20' mi='x2' /> {formatDate(tlm)} </Box>
</Margins>
</Box>
</Container>
<Container alignItems='center'>
<Button rcx-contextual-message__follow small square flexShrink={0} ghost data-following={following} data-id={_id} onClick={handleFollowButton} title={actionLabel} aria-label={actionLabel}><Icon name={button} size='x20'/></Button>
{
(mention && <NotificationStatusMe t={t} mb='x24'/>)
|| (all && <NotificationStatusAll t={t} mb='x24'/>)
|| (unread && <NotificationStatusUnread t={t} mb='x24'/>)
}
</Container>
</Box>;
}
export function MessageSkeleton(props) {
return <Box rcx-message pi='x20' pb='x16' pbs='x16' display='flex' {...props}>
<Container mb='neg-x2'>
<Skeleton variant='rect' size='x36'/>
</Container>
<Container width='1px' mb='neg-x4' flexGrow={1}>
<Header>
<Skeleton width='100%'/>
</Header>
<Body><Skeleton /><Skeleton /></Body>
<Box mi='neg-x8' flexDirection='row' display='flex' alignItems='baseline' mb='x8'>
<Margins inline='x4'>
<Skeleton />
<Skeleton />
<Skeleton />
</Margins>
</Box>
</Container>
</Box>;
}
function Container({ children, ...props }) {
return <Box rcx-message__container display='flex' mi='x4' flexDirection='column' {...props}><Margins block='x2'>{children}</Margins></Box>;
}
function Header({ children }) {
return <Box rcx-message__header display='flex' flexGrow={0} flexShrink={1} withTruncatedText><Box mi='neg-x2' display='flex' flexDirection='row' alignItems='baseline' withTruncatedText flexGrow={1 } flexShrink={1}><Margins inline='x2'> {children} </Margins></Box></Box>;
}
function Username(props) {
return <Box rcx-message__username color='neutral-800' fontSize='x14' fontWeight='600' flexShrink={1} withTruncatedText {...props}/>;
}
function Timestamp({ ts }) {
return <Box rcx-message__time fontSize='c1' color='neutral-600' flexShrink={0} withTruncatedText>{ts.toDateString ? ts.toDateString() : ts }</Box>;
}
const style = {
display: '-webkit-box',
overflow: 'hidden',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
wordBreak: 'break-all',
};
function Body(props) {
return <Box rcx-message__body flexShrink={1} style={style} lineHeight='1.45' minHeight='40px' {...props}/>;
}

@ -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]));

@ -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]));

@ -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() {

@ -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 }) => <WrappedComponent replies={tcount} username={username} msg={msg} ts={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 <WrappedComponent
{...props}
unread={subscription?.tunread}
unreadUser={subscription?.tunreadUser}
unreadGroup={subscription?.tunreadGroup}
userId={userId}
error={error}
discussions={discussions}
total={total}
loading={state === ENDPOINT_STATES.LOADING}
loadMoreItems={loadMoreItems}
room={room}
text={text}
setText={handleTextChange}
/>;
};
}
export const normalizeThreadMessage = ({ ...message }) => {
if (message.msg) {
return renderMessageBody(message).replace(/<br\s?\\?>/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 <Skeleton style={style}/>;
}
const discussion = data[index];
const msg = normalizeThreadMessage(discussion);
const { name = discussion.u.username } = discussion.u;
return <Discussion
{ ...discussion }
name={showRealNames ? name : discussion.u.username }
username={ discussion.u.username }
style={style}
following={discussion.replies && discussion.replies.includes(userId)}
data-drid={discussion.drid}
msg={msg}
t={t}
formatDate={formatDate}
onClick={onClick}
/>;
}), [showRealNames]);
const isItemLoaded = useCallback((index) => index < discussionsRef.current.length, []);
const { ref, contentBoxSize: { inlineSize = 378, blockSize = 750 } = {} } = useResizeObserver();
return <VerticalBar>
<VerticalBar.Header>
<VerticalBar.Icon name='discussion'/>
<Box flexShrink={1} flexGrow={1} withTruncatedText mi='x8'><RawText>{t('Discussions')}</RawText></Box>
<VerticalBar.Close onClick={onClose}/>
</VerticalBar.Header>
<VerticalBar.Content paddingInline={0}>
<Box display='flex' flexDirection='row' p='x24' borderBlockEndWidth='x2' borderBlockEndStyle='solid' borderBlockEndColor='neutral-200'>
<TextInput placeholder={t('Search_Messages')} value={text} onChange={setText} addon={<Icon name='magnifier' size='x20'/>}/>
</Box>
<Box flexGrow={1} flexShrink={1} ref={ref}>
{error && <Callout mi='x24' type='danger'>{error.toString()}</Callout>}
{total === 0 && <Box p='x24'>{t('No_Discussions_found')}</Box>}
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={total}
loadMoreItems={ loading ? () => {} : loadMoreItems}
>
{({ onItemsRendered, ref }) => (<List
height={blockSize}
width={inlineSize}
itemCount={total}
itemData={discussions}
itemSize={124}
ref={ref}
minimumBatchSize={LIST_SIZE}
onItemsRendered={onItemsRendered}
>{rowRenderer}</List>
)}
</InfiniteLoader>
</Box>
</VerticalBar.Content>
</VerticalBar>;
}
export default withData(DiscussionList);

@ -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 <MessageTemplate.Message {...props} className={className}>
<MessageTemplate.Container mb='neg-x2'>
<UserAvatar username={username} rcx-message__avatar size='x36'/>
</MessageTemplate.Container>
<MessageTemplate.Container width='1px' mb='neg-x4' flexGrow={1}>
<MessageTemplate.Header>
<MessageTemplate.Username title={username}>{name}</MessageTemplate.Username>
<MessageTemplate.Timestamp ts={formatDate(ts)}/>
</MessageTemplate.Header>
<MessageTemplate.BodyClamp><Icon name='discussion' size='x20' mie='x2'/><RawText>{msg}</RawText></MessageTemplate.BodyClamp>
<Box mi='neg-x2' flexDirection='row' display='flex' alignItems='baseline' mbs='x8'>
{!dcount && <Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600'>{t('No_messages_yet')}</Box>}
{ !!dcount && <Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600'><Icon name='discussion' size='x20' mi='x2'/>{dcount}</Box>}
{ !!dcount && <Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600' withTruncatedText flexShrink={1} mi='x2'><Icon name='clock' size='x20' mi='x2' /> {formatDate(dlm)} </Box>}
</Box>
</MessageTemplate.Container>
</MessageTemplate.Message>;
});

@ -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 = () => <Message {...message} />;
export const LargeText = () => <Message {...largeText} />;
export const NoReplies = () => <Message {...noReplies} />;

@ -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) => <WrappedComponent className={clickable} tabIndex={0} {...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 }) => <WrappedComponent replies={tcount} participants={replies.length} username={username} msg={msg} ts={ts} {...props}/>;

@ -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 <MessageTemplate.Message {...props} className={[...isIterable(className) ? className : [className], !following && followStyle].filter(Boolean)}>
<MessageTemplate.Container mb='neg-x2'>
<UserAvatar username={username} rcx-message__avatar size='x36'/>
</MessageTemplate.Container>
<MessageTemplate.Container width='1px' mb='neg-x4' flexGrow={1}>
<MessageTemplate.Header>
<MessageTemplate.Username title={username}>{name}</MessageTemplate.Username>
<MessageTemplate.Timestamp ts={formatDate(ts)}/>
</MessageTemplate.Header>
<MessageTemplate.BodyClamp><RawText>{msg}</RawText></MessageTemplate.BodyClamp>
<Box mi='neg-x2' flexDirection='row' display='flex' alignItems='baseline' mbs='x8'>
<Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600'><Icon name='thread' size='x20' mi='x2'/>{replies}</Box>
<Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600'><Icon name='user' size='x20' mi='x2'/>{participants}</Box>
<Box display='flex' alignItems='center' is='span' fontSize='x12' color='neutral-700' fontWeight='600' withTruncatedText flexShrink={1} mi='x2'><Icon name='clock' size='x20' mi='x2' /> {formatDate(tlm)} </Box>
</Box>
</MessageTemplate.Container>
<MessageTemplate.Container alignItems='center'>
<Button rcx-contextual-message__follow small square flexShrink={0} ghost data-following={following} data-id={_id} onClick={handleFollowButton} title={actionLabel} aria-label={actionLabel}><Icon name={button} size='x20'/></Button>
{
(mention && <NotificationStatus.Me t={t} mb='x24'/>)
|| (all && <NotificationStatus.All t={t} mb='x24'/>)
|| (unread && <NotificationStatus.Unread t={t} mb='x24'/>)
}
</MessageTemplate.Container>
</MessageTemplate.Message>;
});

@ -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 = () => <Message {...message} />;
export const LargeText = () => <Message {...largeText} />;
export const Following = () => <Message {...following} />;
export const Unread = () => <Message {...unread} />;
export const Mention = () => <Message {...mention} />;
export const MentionAll = () => <Message {...all} />;

@ -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
});

@ -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 <Message {...props}>
<Container mb='neg-x2'>
<Skeleton variant='rect' size='x36'/>
</Container>
<Container width='1px' mb='neg-x4' flexGrow={1}>
<Header>
<Skeleton width='100%'/>
</Header>
<BodyClamp><Skeleton /><Skeleton /></BodyClamp>
<Box mi='neg-x8' flexDirection='row' display='flex' alignItems='baseline' mb='x8'>
<Margins inline='x4'>
<Skeleton />
<Skeleton />
<Skeleton />
</Margins>
</Box>
</Container>
</Message>;
});
export function Container({ children, ...props }) {
return <Box rcx-message__container display='flex' mi='x4' flexDirection='column' {...props}><Margins block='x2'>{children}</Margins></Box>;
}
export function Header({ children }) {
return <Box rcx-message__header display='flex' flexGrow={0} flexShrink={1} withTruncatedText><Box mi='neg-x2' display='flex' flexDirection='row' alignItems='baseline' withTruncatedText flexGrow={1} flexShrink={1}><Margins inline='x2'> {children} </Margins></Box></Box>;
}
export function Username(props) {
return <Box rcx-message__username color='neutral-800' fontSize='x14' fontWeight='600' flexShrink={1} withTruncatedText {...props}/>;
}
export function Timestamp({ ts }) {
return <Box rcx-message__time fontSize='c1' color='neutral-600' flexShrink={0} withTruncatedText>{ts.toDateString ? ts.toDateString() : ts }</Box>;
}
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 <Box rcx-contextual-message pi='x20' pb='x16' pbs='x16' display='flex' {...props} className={[...isIterable(className) ? className : [className]].filter(Boolean)}/>;
}
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 <Box rcx-message__body className={style} flexShrink={1} lineHeight='1.45' minHeight='40px' {...props}/>;
}

@ -0,0 +1,18 @@
import React from 'react';
import { Box } from '@rocket.chat/fuselage';
export function NotificationStatus({ t = (e) => e, label, ...props }) {
return <Box width='x8' aria-label={t(label)} borderRadius='full' height='x8' {...props} />;
}
export function All(props) {
return <NotificationStatus label='mention-all' bg='#F38C39' {...props} />;
}
export function Me(props) {
return <NotificationStatus label='Me' bg='danger-500' {...props} />;
}
export function Unread(props) {
return <NotificationStatus label='Unread' bg='primary-500' {...props} />;
}

@ -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) => <WrappedComponent className={clickable} tabIndex={0} {...props}/>;
}

@ -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]));

@ -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]));

@ -0,0 +1 @@
import './adapters';

@ -30,3 +30,4 @@ import './startup/unread';
import './startup/userSetUtcOffset';
import './startup/usersObserve';
import './admin';
import './Channel';

@ -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 <strong>\"%s\"</strong> was found!",
"No_data_found": "No data found",

Loading…
Cancel
Save