Skip to content

Commit

Permalink
Feature/new messages indicator (#548)
Browse files Browse the repository at this point in the history
* DateSeparator supports unread prop

* show New indicator in MessageList for unread msgs

* check for undefined lastRead

Co-authored-by: Vinícius Andrade <[email protected]>

Co-authored-by: Vinícius Andrade <[email protected]>
  • Loading branch information
Amin Mahboubi and vini-btc authored Sep 29, 2020
1 parent 75a5dfb commit 2c2180e
Show file tree
Hide file tree
Showing 12 changed files with 94 additions and 14 deletions.
14 changes: 9 additions & 5 deletions src/components/DateSeparator/DateSeparator.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ import { TranslationContext } from '../../context';
* @example ../../docs/DateSeparator.md
* @type {React.FC<import('types').DateSeparatorProps>}
*/
const DateSeparator = ({ position = 'right', formatDate, date }) => {
const { tDateTimeParser } = useContext(TranslationContext);
const DateSeparator = ({ position = 'right', formatDate, date, unread }) => {
const { t, tDateTimeParser } = useContext(TranslationContext);
if (typeof date === 'string') return null;

const formattedDate = formatDate
? formatDate(date)
: tDateTimeParser(date.toISOString()).calendar();

return (
<div className="str-chat__date-separator">
{(position === 'right' || position === 'center') && (
<hr className="str-chat__date-separator-line" />
)}
<div className="str-chat__date-separator-date">
{formatDate
? formatDate(date)
: tDateTimeParser(date.toISOString()).calendar()}
{unread ? t('New') : formattedDate}
</div>
{(position === 'left' || position === 'center') && (
<hr className="str-chat__date-separator-line" />
Expand All @@ -34,6 +36,8 @@ const DateSeparator = ({ position = 'right', formatDate, date }) => {
DateSeparator.propTypes = {
/** The date to format */
date: PropTypes.instanceOf(Date).isRequired,
/** If following messages are not new */
unread: PropTypes.bool,
/** Set the position of the date in the separator */
position: PropTypes.oneOf(['left', 'center', 'right']),
/** Override the default formatting of the date. This is a function that has access to the original date object. Returns a string or Node */
Expand Down
56 changes: 53 additions & 3 deletions src/components/DateSeparator/__tests__/DateSeparator.test.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import React from 'react';
import renderer from 'react-test-renderer';
import Dayjs from 'dayjs';
import calendar from 'dayjs/plugin/calendar';
import { cleanup, render } from '@testing-library/react';
import '@testing-library/jest-dom';

import DateSeparator from '../DateSeparator';
import { TranslationContext } from '../../../context';

Dayjs.extend(calendar);

afterEach(cleanup); // eslint-disable-line

// this changes every time tests are run,
// but by mocking the actual renderers tests are still deterministic
const now = new Date();

const withContext = (props) => {
const t = jest.fn((key) => key);
const tDateTimeParser = jest.fn((input) => Dayjs(input));
const Component = (
<TranslationContext.Provider value={{ t, tDateTimeParser }}>
<DateSeparator {...props} />
</TranslationContext.Provider>
);

return { Component, t, tDateTimeParser };
};

describe('DateSeparator', () => {
it('should use formatDate if it is provided', () => {
const { queryByText } = render(
Expand All @@ -20,9 +37,42 @@ describe('DateSeparator', () => {
expect(queryByText('the date')).toBeInTheDocument();
});

it.todo(
"should use tDateTimeParser's calendar method to format dates if formatDate prop is not specified",
);
it('should render New text if unread prop is true', () => {
const { Component, t } = withContext({ date: now, unread: true });
const { queryByText } = render(Component);

expect(queryByText('New')).toBeInTheDocument();
expect(t).toHaveBeenCalledWith('New');
});

it('should render properly for unread', () => {
const { Component } = withContext({ date: now, unread: true });
const tree = renderer.create(Component).toJSON();
expect(tree).toMatchInlineSnapshot(`
<div
className="str-chat__date-separator"
>
<hr
className="str-chat__date-separator-line"
/>
<div
className="str-chat__date-separator-date"
>
New
</div>
</div>
`);
});

it("should use tDateTimeParser's calendar method by default", () => {
const { Component, tDateTimeParser } = withContext({ date: now });
const { queryByText } = render(Component);

expect(tDateTimeParser).toHaveBeenCalledWith(now.toISOString());
expect(
queryByText(Dayjs(now.toISOString()).calendar()),
).toBeInTheDocument();
});

describe('Position prop', () => {
const renderWithPosition = (position) => (
Expand Down
1 change: 1 addition & 0 deletions src/components/MessageList/MessageList.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ class MessageList extends PureComponent {
noGroupByUser={this.props.noGroupByUser}
threadList={this.props.threadList}
client={this.props.client}
channel={this.props.channel}
read={this.props.read}
bottomRef={this.bottomRef}
onMessageLoadCaptured={this.onMessageLoadCaptured}
Expand Down
28 changes: 22 additions & 6 deletions src/components/MessageList/MessageListInner.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const getReadStates = (messages, read) => {
return readData;
};

const insertDates = (messages) => {
const insertDates = (messages, lastRead, userID) => {
let unread = false;
const newMessages = [];
for (let i = 0, l = messages.length; i < l; i += 1) {
const message = messages[i];
Expand All @@ -49,7 +50,21 @@ const insertDates = (messages) => {
prevMessageDate = messages[i - 1].created_at.toDateString();
}

if (i === 0 || messageDate !== prevMessageDate) {
if (!unread) {
unread = lastRead && lastRead.getTime() < message.created_at.getTime();
// userId check makes sure New is not shown for current user messages
if (unread && message.user.id !== userID)
newMessages.push({
type: 'message.date',
date: message.created_at,
unread,
});
}

if (
(i === 0 || messageDate !== prevMessageDate) &&
newMessages?.[newMessages.length - 1]?.type !== 'message.date' // prevent two subsequent DateSeparator
) {
newMessages.push(
{ type: 'message.date', date: message.created_at },
message,
Expand Down Expand Up @@ -173,17 +188,18 @@ const MessageListInner = (props) => {
noGroupByUser,
client,
threadList,
channel,
read,
internalMessageProps,
internalInfiniteScrollProps,
} = props;
const lastRead = useMemo(() => channel.lastRead(), [channel]);

const enrichedMessages = useMemo(() => {
const messageWithDates = insertDates(messages);
// messageWithDates.sort((a, b) => a.created_at - b.created_at); // TODO: remove if no issue came up
const messageWithDates = insertDates(messages, lastRead, client.userID);
if (HeaderComponent) return insertIntro(messageWithDates, headerPosition);
return messageWithDates;
}, [HeaderComponent, headerPosition, messages]);
}, [messages, lastRead, client.userID, HeaderComponent, headerPosition]);

const messageGroupStyles = useMemo(
() =>
Expand Down Expand Up @@ -221,7 +237,7 @@ const MessageListInner = (props) => {

return (
<li key={`${message.date.toISOString()}-i`}>
<DateSeparator date={message.date} />
<DateSeparator date={message.date} unread={message.unread} />
</li>
);
}
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Message failed. Click to try again.": "Message failed. Click to try again.",
"Message has been successfully flagged": "Message has been successfully flagged",
"Mute": "Mute",
"New": "New",
"New Messages!": "New Messages!",
"Nothing yet...": "Nothing yet...",
"Only visible to you": "Only visible to you",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Message failed. Click to try again.": "Échec de l'envoi du message - Cliquez pour réessayer",
"Message has been successfully flagged": "Le message a été signalé avec succès",
"Mute": "Muet",
"New": "Nouveaux",
"New Messages!": "Nouveaux Messages!",
"Nothing yet...": "Aucun message...",
"Only visible to you": "Visible uniquement pour vous",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Message failed. Click to try again.": "मैसेज फ़ैल - पुनः कोशिश करें",
"Message has been successfully flagged": "मैसेज को फ्लैग कर दिया गया है",
"Mute": "म्यूट करे",
"New": "नए",
"New Messages!": "नए मैसेज!",
"Nothing yet...": "कोई मैसेज नहीं है",
"Only visible to you": "सिर्फ आपको दिखाई दे रहा है",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Message failed. Click to try again.": "Invio messaggio fallito. Clicca per riprovare.",
"Message has been successfully flagged": "Il messaggio é stato segnalato con successo",
"Mute": "Silenzia",
"New": "Nuovo",
"New Messages!": "Nuovo messaggio!",
"Nothing yet...": "Ancora niente...",
"Only visible to you": "Visibile soltanto da te",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Message failed. Click to try again.": "Bericht mislukt, klik om het nogmaals te proberen",
"Message has been successfully flagged": "Bericht is succesvol gemarkeerd",
"Mute": "Mute",
"New": "Nieuwe",
"New Messages!": "Nieuwe Berichten!",
"Nothing yet...": "Nog niets ...",
"Only visible to you": "Alleen zichtbaar voor jou",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Message failed. Click to try again.": "Ошибка отправки сообщения · Нажмите чтобы повторить",
"Message has been successfully flagged": "Жалоба на сообщение была принята",
"Mute": "Отключить уведомления",
"New": "Новые",
"New Messages!": "Новые сообщения!",
"Nothing yet...": "Пока ничего нет...",
"Only visible to you": "Только видно для вас",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Message failed. Click to try again.": "Mesaj başarısız oldu. Tekrar denemek için tıklayın",
"Message has been successfully flagged": "Mesaj başarıyla bayraklandı",
"Mute": "Sessiz",
"New": "Yeni",
"New Messages!": "Yeni Mesajlar!",
"Nothing yet...": "Şimdilik hiçbir şey...",
"Only visible to you": "Sadece size görünür",
Expand Down
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ export interface AvatarProps {
export interface DateSeparatorProps extends TranslationContextValue {
/** The date to format */
date: Date;
/** If following messages are not new */
unread?: boolean;
/** Set the position of the date in the separator */
position?: 'left' | 'center' | 'right';
/** Override the default formatting of the date. This is a function that has access to the original date object. Returns a string or Node */
Expand Down

0 comments on commit 2c2180e

Please sign in to comment.