From 36dd3f19e27d97e0885064e56c5a786ced10fda1 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Mon, 9 Jul 2018 18:35:43 -0400 Subject: [PATCH] MM-11198: avoid duplicate date line keys (#1901) * MM-11198: avoid duplicate keys in date lines Ensure each dateline rendered as part of search, recent mentions, or flagged post list has a unique key, even if the same date itself appears multiple times. * update post_list.test.js to actually test date boundaries, relying on TZ=utc export in package.json * unit test makePreparePostIdsForSearchPosts * fix eslint issues * try TZ=GMT * encode date lines using timestamps instead This requires a few extra changes to the post list proper, but largely simplifies things anyway. --- .../__snapshots__/date_header.test.js.snap | 641 ++++++++++++++++++ .../post_list/date_header/date_header.js | 14 +- .../post_list/date_header/date_header.test.js | 47 ++ app/components/post_list/date_header/utils.js | 6 + .../post_list/date_header/utils.test.js | 17 + app/components/post_list/post_list.js | 24 +- app/screens/flagged_posts/flagged_posts.js | 11 +- .../recent_mentions/recent_mentions.js | 11 +- app/screens/search/search.js | 24 +- app/selectors/post_list.js | 12 +- app/selectors/post_list.test.js | 273 +++++++- 11 files changed, 1003 insertions(+), 77 deletions(-) create mode 100644 app/components/post_list/date_header/__snapshots__/date_header.test.js.snap create mode 100644 app/components/post_list/date_header/date_header.test.js create mode 100644 app/components/post_list/date_header/utils.js create mode 100644 app/components/post_list/date_header/utils.test.js diff --git a/app/components/post_list/date_header/__snapshots__/date_header.test.js.snap b/app/components/post_list/date_header/__snapshots__/date_header.test.js.snap new file mode 100644 index 000000000..381ea86a7 --- /dev/null +++ b/app/components/post_list/date_header/__snapshots__/date_header.test.js.snap @@ -0,0 +1,641 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DateHeader component should match snapshot with suffix 1`] = ` +ShallowWrapper { + "length": 1, + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + , + , + ], + "style": Array [ + Object { + "alignItems": "center", + "flexDirection": "row", + "height": 28, + }, + undefined, + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "#aaa", + "flex": 1, + "height": 1, + "opacity": 0.2, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": , + "style": Object { + "marginHorizontal": 15, + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "day": "2-digit", + "month": "short", + "style": Object { + "color": "#aaa", + "fontSize": 14, + "fontWeight": "600", + }, + "value": 1970-01-18T17:19:12.392Z, + "weekday": "short", + "year": "numeric", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "#aaa", + "flex": 1, + "height": 1, + "opacity": 0.2, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + , + , + ], + "style": Array [ + Object { + "alignItems": "center", + "flexDirection": "row", + "height": 28, + }, + undefined, + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "#aaa", + "flex": 1, + "height": 1, + "opacity": 0.2, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": , + "style": Object { + "marginHorizontal": 15, + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "day": "2-digit", + "month": "short", + "style": Object { + "color": "#aaa", + "fontSize": 14, + "fontWeight": "600", + }, + "value": 1970-01-18T17:19:12.392Z, + "weekday": "short", + "year": "numeric", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "#aaa", + "flex": 1, + "height": 1, + "opacity": 0.2, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + }, + }, + "context": Object { + "intl": Object { + "formatMessage": [MockFunction], + }, + }, + }, +} +`; + +exports[`DateHeader component should match snapshot without suffix 1`] = ` +ShallowWrapper { + "length": 1, + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + , + , + ], + "style": Array [ + Object { + "alignItems": "center", + "flexDirection": "row", + "height": 28, + }, + undefined, + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "#aaa", + "flex": 1, + "height": 1, + "opacity": 0.2, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": , + "style": Object { + "marginHorizontal": 15, + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "day": "2-digit", + "month": "short", + "style": Object { + "color": "#aaa", + "fontSize": 14, + "fontWeight": "600", + }, + "value": 1970-01-18T17:19:12.392Z, + "weekday": "short", + "year": "numeric", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "#aaa", + "flex": 1, + "height": 1, + "opacity": 0.2, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + , + , + ], + "style": Array [ + Object { + "alignItems": "center", + "flexDirection": "row", + "height": 28, + }, + undefined, + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "#aaa", + "flex": 1, + "height": 1, + "opacity": 0.2, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": , + "style": Object { + "marginHorizontal": 15, + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "day": "2-digit", + "month": "short", + "style": Object { + "color": "#aaa", + "fontSize": 14, + "fontWeight": "600", + }, + "value": 1970-01-18T17:19:12.392Z, + "weekday": "short", + "year": "numeric", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "style": Object { + "backgroundColor": "#aaa", + "flex": 1, + "height": 1, + "opacity": 0.2, + }, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + }, + }, + "context": Object { + "intl": Object { + "formatMessage": [MockFunction], + }, + }, + }, +} +`; diff --git a/app/components/post_list/date_header/date_header.js b/app/components/post_list/date_header/date_header.js index 9e5d2c47f..1911832b2 100644 --- a/app/components/post_list/date_header/date_header.js +++ b/app/components/post_list/date_header/date_header.js @@ -10,17 +10,27 @@ import { import FormattedDate from 'app/components/formatted_date'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; +import {DATE_LINE, DATE_LINE_SUFFIX} from 'app/selectors/post_list'; +// DateHeader accepts as a timestamp encoded as a string for rendering as part of a post list. export default class DateHeader extends PureComponent { static propTypes = { - date: PropTypes.object.isRequired, + dateLineString: PropTypes.string.isRequired, theme: PropTypes.object.isRequired, style: ViewPropTypes.style, }; render() { - const {date, theme} = this.props; + const {theme, dateLineString} = this.props; const style = getStyleSheet(theme); + const indexSuffix = dateLineString.indexOf(DATE_LINE_SUFFIX); + + let date; + if (indexSuffix >= 0) { + date = new Date(parseInt(dateLineString.substring(DATE_LINE.length, indexSuffix), 10)); + } else { + date = new Date(parseInt(dateLineString.substring(DATE_LINE.length), 10)); + } return ( diff --git a/app/components/post_list/date_header/date_header.test.js b/app/components/post_list/date_header/date_header.test.js new file mode 100644 index 000000000..e7b9e2898 --- /dev/null +++ b/app/components/post_list/date_header/date_header.test.js @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable max-nested-callbacks */ + +import React from 'react'; +import {configure, shallow} from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +configure({adapter: new Adapter()}); + +import DateHeader from './date_header.js'; + +describe('DateHeader', () => { + const baseProps = { + theme: {centerChannelBg: '#aaa', centerChannelColor: '#aaa'}, + }; + + describe('component should match snapshot', () => { + it('without suffix', () => { + const props = { + ...baseProps, + dateLineString: 'date-1531152392', + index: 0, + }; + const wrapper = shallow( + , + {context: {intl: {formatMessage: jest.fn()}}}, + ); + + expect(wrapper).toMatchSnapshot(); + }); + + it('with suffix', () => { + const props = { + ...baseProps, + dateLineString: 'date-1531152392-index-2', + index: 2, + }; + const wrapper = shallow( + , + {context: {intl: {formatMessage: jest.fn()}}}, + ); + + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/app/components/post_list/date_header/utils.js b/app/components/post_list/date_header/utils.js new file mode 100644 index 000000000..c08f750da --- /dev/null +++ b/app/components/post_list/date_header/utils.js @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {DATE_LINE} from 'app/selectors/post_list'; + +export const isDateLine = (dateString) => dateString.indexOf(DATE_LINE) === 0; diff --git a/app/components/post_list/date_header/utils.test.js b/app/components/post_list/date_header/utils.test.js new file mode 100644 index 000000000..c27de2ee7 --- /dev/null +++ b/app/components/post_list/date_header/utils.test.js @@ -0,0 +1,17 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import assert from 'assert'; + +import {isDateLine} from './utils.js'; + +describe('DateHeader', () => { + it('isDateLine', () => { + assert.equal(isDateLine(''), false); + assert.equal(isDateLine('date'), false); + assert.equal(isDateLine('date-'), true); + assert.equal(isDateLine('date-0'), true); + assert.equal(isDateLine('date-1531152392'), true); + assert.equal(isDateLine('date-1531152392-index'), true); + }); +}); diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index ac8be1af8..51b205317 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -13,12 +13,13 @@ import { import EventEmitter from 'mattermost-redux/utils/event_emitter'; import Post from 'app/components/post'; -import {DATE_LINE, START_OF_NEW_MESSAGES} from 'app/selectors/post_list'; +import {START_OF_NEW_MESSAGES} from 'app/selectors/post_list'; import mattermostManaged from 'app/mattermost_managed'; import {makeExtraData} from 'app/utils/list_view'; import {changeOpacity} from 'app/utils/theme'; import DateHeader from './date_header'; +import {isDateLine} from './date_header/utils'; import NewMessagesDivider from './new_messages_divider'; import withLayout from './with_layout'; @@ -264,9 +265,14 @@ export default class PostList extends PureComponent { moreMessages={this.moreNewMessages} /> ); - } else if (item.indexOf(DATE_LINE) === 0) { - const date = item.substring(DATE_LINE.length); - return this.renderDateHeader(new Date(date), index); + } else if (isDateLine(item)) { + this.itemMeasurements[index] = DATE_HEADER_HEIGHT; + return ( + + ); } const postId = item; @@ -279,16 +285,6 @@ export default class PostList extends PureComponent { return this.renderPost(postId, previousPostId, nextPostId, index); }; - renderDateHeader = (date, index) => { - this.itemMeasurements[index] = DATE_HEADER_HEIGHT; - return ( - - ); - }; - renderPost = (postId, previousPostId, nextPostId, index) => { const { highlightPostId, diff --git a/app/screens/flagged_posts/flagged_posts.js b/app/screens/flagged_posts/flagged_posts.js index 0b560a3b0..f329083a7 100644 --- a/app/screens/flagged_posts/flagged_posts.js +++ b/app/screens/flagged_posts/flagged_posts.js @@ -14,6 +14,7 @@ import { import ChannelLoader from 'app/components/channel_loader'; import DateHeader from 'app/components/post_list/date_header'; +import {isDateLine} from 'app/components/post_list/date_header/utils'; import FailedNetworkAction from 'app/components/failed_network_action'; import NoResults from 'app/components/no_results'; import PostSeparator from 'app/components/post_separator'; @@ -21,7 +22,6 @@ import StatusBar from 'app/components/status_bar'; import mattermostManaged from 'app/mattermost_managed'; import SearchResultPost from 'app/screens/search/search_result_post'; import ChannelDisplayName from 'app/screens/search/channel_display_name'; -import {DATE_LINE} from 'app/selectors/post_list'; import {changeOpacity} from 'app/utils/theme'; export default class FlaggedPosts extends PureComponent { @@ -150,13 +150,10 @@ export default class FlaggedPosts extends PureComponent { renderPost = ({item, index}) => { const {postIds, theme} = this.props; const {managedConfig} = this.state; - if (item.indexOf(DATE_LINE) === 0) { - const date = new Date(item.substring(DATE_LINE.length)); - + if (isDateLine(item)) { return ( ); @@ -164,7 +161,7 @@ export default class FlaggedPosts extends PureComponent { let separator; const nextPost = postIds[index + 1]; - if (nextPost && nextPost.indexOf(DATE_LINE) === -1) { + if (nextPost && !isDateLine(nextPost)) { separator = ; } diff --git a/app/screens/recent_mentions/recent_mentions.js b/app/screens/recent_mentions/recent_mentions.js index 541b9e452..ed6e3b0f7 100644 --- a/app/screens/recent_mentions/recent_mentions.js +++ b/app/screens/recent_mentions/recent_mentions.js @@ -14,6 +14,7 @@ import { import ChannelLoader from 'app/components/channel_loader'; import DateHeader from 'app/components/post_list/date_header'; +import {isDateLine} from 'app/components/post_list/date_header/utils'; import FailedNetworkAction from 'app/components/failed_network_action'; import NoResults from 'app/components/no_results'; import PostSeparator from 'app/components/post_separator'; @@ -21,7 +22,6 @@ import StatusBar from 'app/components/status_bar'; import mattermostManaged from 'app/mattermost_managed'; import SearchResultPost from 'app/screens/search/search_result_post'; import ChannelDisplayName from 'app/screens/search/channel_display_name'; -import {DATE_LINE} from 'app/selectors/post_list'; import {changeOpacity} from 'app/utils/theme'; export default class RecentMentions extends PureComponent { @@ -150,13 +150,10 @@ export default class RecentMentions extends PureComponent { renderPost = ({item, index}) => { const {postIds, theme} = this.props; const {managedConfig} = this.state; - if (item.indexOf(DATE_LINE) === 0) { - const date = new Date(item.substring(DATE_LINE.length)); - + if (isDateLine(item)) { return ( ); @@ -164,7 +161,7 @@ export default class RecentMentions extends PureComponent { let separator; const nextPost = postIds[index + 1]; - if (nextPost && nextPost.indexOf(DATE_LINE) === -1) { + if (nextPost && !isDateLine(nextPost)) { separator = ; } diff --git a/app/screens/search/search.js b/app/screens/search/search.js index cd16ad81d..7e17ba525 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -21,6 +21,7 @@ import {RequestStatus} from 'mattermost-redux/constants'; import Autocomplete from 'app/components/autocomplete'; import DateHeader from 'app/components/post_list/date_header'; +import {isDateLine} from 'app/components/post_list/date_header/utils'; import FormattedText from 'app/components/formatted_text'; import Loading from 'app/components/loading'; import PostListRetry from 'app/components/post_list_retry'; @@ -29,7 +30,6 @@ import SafeAreaView from 'app/components/safe_area_view'; import SearchBar from 'app/components/search_bar'; import StatusBar from 'app/components/status_bar'; import mattermostManaged from 'app/mattermost_managed'; -import {DATE_LINE} from 'app/selectors/post_list'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; @@ -260,16 +260,6 @@ export default class Search extends PureComponent { actions.removeSearchTerms(currentTeamId, item.terms); }); - renderDateHeader = (date, index) => { - return ( - - ); - }; - renderModifiers = ({item}) => { const {theme} = this.props; const style = getStyleFromTheme(theme); @@ -317,14 +307,18 @@ export default class Search extends PureComponent { ); } - if (item.indexOf(DATE_LINE) === 0) { - const date = item.substring(DATE_LINE.length); - return this.renderDateHeader(new Date(date), index); + if (isDateLine(item)) { + return ( + + ); } let separator; const nextPost = postIds[index + 1]; - if (nextPost && nextPost.indexOf(DATE_LINE) === -1) { + if (nextPost && !isDateLine(nextPost)) { separator = ; } diff --git a/app/selectors/post_list.js b/app/selectors/post_list.js index b138aa6f8..ab738a329 100644 --- a/app/selectors/post_list.js +++ b/app/selectors/post_list.js @@ -9,6 +9,7 @@ import {createIdsSelector} from 'mattermost-redux/utils/helpers'; import {shouldFilterJoinLeavePost} from 'mattermost-redux/utils/post_utils'; export const DATE_LINE = 'date-'; +export const DATE_LINE_SUFFIX = '-index-'; export const START_OF_NEW_MESSAGES = 'start-of-new-messages'; function shouldShowJoinLeaveMessages(state) { @@ -56,7 +57,7 @@ export function makePreparePostIdsForPostList() { const postDate = new Date(post.create_at); if (!lastDate || lastDate.toDateString() !== postDate.toDateString()) { - out.push(DATE_LINE + postDate.toString()); + out.push(DATE_LINE + post.create_at); lastDate = postDate; } @@ -105,9 +106,14 @@ export function makePreparePostIdsForSearchPosts() { continue; } - const postDate = new Date(post.create_at); + // Render a date line for each post, even if displayed on the same date as the + // previous post. Since we don't deduplicate here like in other views, we need to + // ensure the resulting key is unique, even if the post timestamps (down to the + // second) are identical. The screens know to parse out the index before trying + // to consume the date value. + out.push(DATE_LINE + post.create_at + DATE_LINE_SUFFIX + i); - out.push(DATE_LINE + postDate.toString(), post.id); + out.push(post.id); } return out; diff --git a/app/selectors/post_list.test.js b/app/selectors/post_list.test.js index d72a1ab52..d69965e00 100644 --- a/app/selectors/post_list.test.js +++ b/app/selectors/post_list.test.js @@ -4,15 +4,15 @@ import assert from 'assert'; import { - DATE_LINE, makePreparePostIdsForPostList, + makePreparePostIdsForSearchPosts, START_OF_NEW_MESSAGES, } from 'app/selectors/post_list'; import {Posts, Preferences} from 'mattermost-redux/constants'; import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils'; -/* eslint-disable max-nested-callbacks, no-console */ +/* eslint-disable max-nested-callbacks */ describe('Selectors.PostList', () => { describe('makePreparePostIdsForPostList', () => { @@ -44,7 +44,11 @@ describe('Selectors.PostList', () => { // Defaults to show post let now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages}); - assert.deepEqual(removeDateLines(now), ['1002', '1001']); + assert.deepEqual(now, [ + '1002', + '1001', + 'date-0', + ]); // Show join/leave posts state = { @@ -66,7 +70,11 @@ describe('Selectors.PostList', () => { }; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages}); - assert.deepEqual(removeDateLines(now), ['1002', '1001']); + assert.deepEqual(now, [ + '1002', + '1001', + 'date-0', + ]); // Hide join/leave posts state = { @@ -88,7 +96,10 @@ describe('Selectors.PostList', () => { }; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages}); - assert.deepEqual(removeDateLines(now), ['1001']); + assert.deepEqual(now, [ + '1001', + 'date-0', + ]); // always show join/leave posts for the current user state = { @@ -107,7 +118,11 @@ describe('Selectors.PostList', () => { now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages}); - assert.deepEqual(removeDateLines(now), ['1002', '1001']); + assert.deepEqual(now, [ + '1002', + '1001', + 'date-0', + ]); }); it('new messages indicator', () => { @@ -138,28 +153,66 @@ describe('Selectors.PostList', () => { // Do not show new messages indicator before all posts let now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 0, indicateNewMessages: true}); - assert.deepEqual(removeDateLines(now), ['1010', '1005', '1000']); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + 'date-1000', + ]); now = preparePostIdsForPostList(state, {postIds, indicateNewMessages: true}); - assert.deepEqual(removeDateLines(now), ['1010', '1005', '1000']); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + 'date-1000', + ]); now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 999, indicateNewMessages: false}); - assert.deepEqual(removeDateLines(now), ['1010', '1005', '1000']); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + 'date-1000', + ]); // Show new messages indicator before all posts now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 999, indicateNewMessages: true}); - assert.deepEqual(removeDateLines(now), ['1010', '1005', '1000', START_OF_NEW_MESSAGES]); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + START_OF_NEW_MESSAGES, + 'date-1000', + ]); // Show indicator between posts now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 1003, indicateNewMessages: true}); - assert.deepEqual(removeDateLines(now), ['1010', '1005', START_OF_NEW_MESSAGES, '1000']); + assert.deepEqual(now, [ + '1010', + '1005', + START_OF_NEW_MESSAGES, + '1000', + 'date-1000', + ]); now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 1006, indicateNewMessages: true}); - assert.deepEqual(removeDateLines(now), ['1010', START_OF_NEW_MESSAGES, '1005', '1000']); + assert.deepEqual(now, [ + '1010', + START_OF_NEW_MESSAGES, + '1005', + '1000', + 'date-1000', + ]); // Don't show indicator when all posts are read now = preparePostIdsForPostList(state, {postIds, lastViewedAt: 1020}); - assert.deepEqual(removeDateLines(now), ['1010', '1005', '1000']); + assert.deepEqual(now, [ + '1010', + '1005', + '1000', + 'date-1000', + ]); }); it('memoization', () => { @@ -197,17 +250,38 @@ describe('Selectors.PostList', () => { }, }; - let postIds = ['1006', '1004', '1003', '1001']; + let postIds = [ + '1006', + '1004', + '1003', + '1001', + ]; let lastViewedAt = initialPosts['1001'].create_at + 1; let now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); - assert.deepEqual(removeDateLines(now), ['1006', '1004', '1003', START_OF_NEW_MESSAGES, '1001']); + assert.deepEqual(now, [ + '1006', + '1004', + 'date-90000000', + '1003', + START_OF_NEW_MESSAGES, + '1001', + 'date-3600000', + ]); // No changes let prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.equal(now, prev); - assert.deepEqual(removeDateLines(now), ['1006', '1004', '1003', START_OF_NEW_MESSAGES, '1001']); + assert.deepEqual(now, [ + '1006', + '1004', + 'date-90000000', + '1003', + START_OF_NEW_MESSAGES, + '1001', + 'date-3600000', + ]); // lastViewedAt changed slightly lastViewedAt = initialPosts['1001'].create_at + 2; @@ -215,7 +289,15 @@ describe('Selectors.PostList', () => { prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.equal(now, prev); - assert.deepEqual(removeDateLines(now), ['1006', '1004', '1003', START_OF_NEW_MESSAGES, '1001']); + assert.deepEqual(now, [ + '1006', + '1004', + 'date-90000000', + '1003', + START_OF_NEW_MESSAGES, + '1001', + 'date-3600000', + ]); // lastViewedAt changed a lot lastViewedAt += initialPosts['1003'].create_at + 1; @@ -223,12 +305,28 @@ describe('Selectors.PostList', () => { prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.notEqual(now, prev); - assert.deepEqual(removeDateLines(now), ['1006', '1004', START_OF_NEW_MESSAGES, '1003', '1001']); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-90000000', + '1003', + '1001', + 'date-3600000', + ]); prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.equal(now, prev); - assert.deepEqual(removeDateLines(now), ['1006', '1004', START_OF_NEW_MESSAGES, '1003', '1001']); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-90000000', + '1003', + '1001', + 'date-3600000', + ]); // postIds changed, but still shallowly equal postIds = [...postIds]; @@ -236,7 +334,15 @@ describe('Selectors.PostList', () => { prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.equal(now, prev); - assert.deepEqual(removeDateLines(now), ['1006', '1004', START_OF_NEW_MESSAGES, '1003', '1001']); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-90000000', + '1003', + '1001', + 'date-3600000', + ]); // Post changed, not in postIds state = { @@ -256,7 +362,15 @@ describe('Selectors.PostList', () => { prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.equal(now, prev); - assert.deepEqual(removeDateLines(now), ['1006', '1004', START_OF_NEW_MESSAGES, '1003', '1001']); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-90000000', + '1003', + '1001', + 'date-3600000', + ]); // Post changed, in postIds state = { @@ -276,7 +390,15 @@ describe('Selectors.PostList', () => { prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.equal(now, prev); - assert.deepEqual(removeDateLines(now), ['1006', '1004', START_OF_NEW_MESSAGES, '1003', '1001']); + assert.deepEqual(now, [ + '1006', + '1004', + START_OF_NEW_MESSAGES, + 'date-90000000', + '1003', + '1001', + 'date-3600000', + ]); // Filter changed state = { @@ -300,17 +422,110 @@ describe('Selectors.PostList', () => { prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.notEqual(now, prev); - assert.deepEqual(removeDateLines(now), ['1004', START_OF_NEW_MESSAGES, '1003', '1001']); + assert.deepEqual(now, [ + '1004', + START_OF_NEW_MESSAGES, + 'date-90000000', + '1003', + '1001', + 'date-3600000', + ]); prev = now; now = preparePostIdsForPostList(state, {postIds, lastViewedAt, indicateNewMessages: true}); assert.equal(now, prev); - assert.deepEqual(removeDateLines(now), ['1004', START_OF_NEW_MESSAGES, '1003', '1001']); + assert.deepEqual(now, [ + '1004', + START_OF_NEW_MESSAGES, + 'date-90000000', + '1003', + '1001', + 'date-3600000', + ]); + }); + }); + + describe('makePreparePostIdsForSearchPosts', () => { + it('should return an empty array if there are no posts specified', () => { + const preparePostIdsForSearchPosts = makePreparePostIdsForSearchPosts(); + + const state = { + entities: { + posts: { + posts: { + 1001: {id: '1001', create_at: 0, type: ''}, + 1002: {id: '1002', create_at: 1, type: ''}, + }, + }, + users: { + currentUserId: '1234', + profiles: { + 1234: {id: '1234', username: 'user'}, + }, + }, + }, + }; + + const postIds = []; + const actual = preparePostIdsForSearchPosts(state, postIds); + assert.deepEqual(actual, []); + }); + + it('should return an empty array if there are no matching posts', () => { + const preparePostIdsForSearchPosts = makePreparePostIdsForSearchPosts(); + + const state = { + entities: { + posts: { + posts: {}, + }, + users: { + currentUserId: '1234', + profiles: { + 1234: {id: '1234', username: 'user'}, + }, + }, + }, + }; + + const postIds = ['1002', '1001']; + const actual = preparePostIdsForSearchPosts(state, postIds); + assert.deepEqual(actual, []); + }); + + it('should return results when there are posts', () => { + const preparePostIdsForSearchPosts = makePreparePostIdsForSearchPosts(); + + const state = { + entities: { + posts: { + posts: { + 1001: {id: '1001', create_at: 0, type: ''}, + 1002: {id: '1002', create_at: 1000, type: ''}, + + // Same timestamp as 1002 + 1003: {id: '1003', create_at: 1000, type: ''}, + }, + }, + users: { + currentUserId: '1234', + profiles: { + 1234: {id: '1234', username: 'user'}, + }, + }, + }, + }; + + const postIds = ['1003', '1002', '1001']; + const actual = preparePostIdsForSearchPosts(state, postIds); + assert.deepEqual(actual, [ + 'date-1000-index-0', + '1003', + 'date-1000-index-1', + '1002', + 'date-0-index-2', + '1001', + ]); }); }); }); - -// Remove date lines when checking equality since those depend on time-zone of the computer running the tests -function removeDateLines(list) { - return list.filter((item) => !item.startsWith(DATE_LINE)); -}