From 2c2180ed10d0b8afaa90d17779e09e1de5bca010 Mon Sep 17 00:00:00 2001 From: Amin Mahboubi Date: Tue, 29 Sep 2020 09:03:24 +0200 Subject: [PATCH] Feature/new messages indicator (#548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * DateSeparator supports unread prop * show New indicator in MessageList for unread msgs * check for undefined lastRead Co-authored-by: Vinícius Andrade Co-authored-by: Vinícius Andrade --- src/components/DateSeparator/DateSeparator.js | 14 +++-- .../__tests__/DateSeparator.test.js | 56 ++++++++++++++++++- src/components/MessageList/MessageList.js | 1 + .../MessageList/MessageListInner.js | 28 ++++++++-- src/i18n/en.json | 1 + src/i18n/fr.json | 1 + src/i18n/hi.json | 1 + src/i18n/it.json | 1 + src/i18n/nl.json | 1 + src/i18n/ru.json | 1 + src/i18n/tr.json | 1 + types/index.d.ts | 2 + 12 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/components/DateSeparator/DateSeparator.js b/src/components/DateSeparator/DateSeparator.js index 06999f739..9c069b79f 100644 --- a/src/components/DateSeparator/DateSeparator.js +++ b/src/components/DateSeparator/DateSeparator.js @@ -10,19 +10,21 @@ import { TranslationContext } from '../../context'; * @example ../../docs/DateSeparator.md * @type {React.FC} */ -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 (
{(position === 'right' || position === 'center') && (
)}
- {formatDate - ? formatDate(date) - : tDateTimeParser(date.toISOString()).calendar()} + {unread ? t('New') : formattedDate}
{(position === 'left' || position === 'center') && (
@@ -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 */ diff --git a/src/components/DateSeparator/__tests__/DateSeparator.test.js b/src/components/DateSeparator/__tests__/DateSeparator.test.js index 01f0bd431..7372c1e82 100644 --- a/src/components/DateSeparator/__tests__/DateSeparator.test.js +++ b/src/components/DateSeparator/__tests__/DateSeparator.test.js @@ -1,9 +1,14 @@ 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 @@ -11,6 +16,18 @@ afterEach(cleanup); // eslint-disable-line // 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 = ( + + + + ); + + return { Component, t, tDateTimeParser }; +}; + describe('DateSeparator', () => { it('should use formatDate if it is provided', () => { const { queryByText } = render( @@ -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(` +
+
+
+ New +
+
+ `); + }); + + 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) => ( diff --git a/src/components/MessageList/MessageList.js b/src/components/MessageList/MessageList.js index c2d97e01e..9624509da 100644 --- a/src/components/MessageList/MessageList.js +++ b/src/components/MessageList/MessageList.js @@ -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} diff --git a/src/components/MessageList/MessageListInner.js b/src/components/MessageList/MessageListInner.js index 4d1caa2b3..8fd803d4a 100644 --- a/src/components/MessageList/MessageListInner.js +++ b/src/components/MessageList/MessageListInner.js @@ -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]; @@ -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, @@ -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( () => @@ -221,7 +237,7 @@ const MessageListInner = (props) => { return (
  • - +
  • ); } diff --git a/src/i18n/en.json b/src/i18n/en.json index 3dd810647..911969559 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -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", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 82634bc9a..a48aff4c4 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -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", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index 5b54eacbd..80d2f41da 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -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": "सिर्फ आपको दिखाई दे रहा है", diff --git a/src/i18n/it.json b/src/i18n/it.json index 44749abed..ce1569fee 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -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", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 84f52cf2f..d752632da 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -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", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 0c520825d..a39993deb 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -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": "Только видно для вас", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 90cda8e00..dd8b65db9 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -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", diff --git a/types/index.d.ts b/types/index.d.ts index 860a38764..36ee2c4c3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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 */