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.
This commit is contained in:
Jesse Hallam 2018-07-09 18:35:43 -04:00 committed by Elias Nahum
parent ca4d08fc7e
commit 36dd3f19e2
11 changed files with 1003 additions and 77 deletions

View file

@ -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__): <DateHeader
dateLineString="date-1531152392-index-2"
index={2}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
}
}
/>,
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 [
<Component
style={
Object {
"backgroundColor": "#aaa",
"flex": 1,
"height": 1,
"opacity": 0.2,
}
}
/>,
<Component
style={
Object {
"marginHorizontal": 15,
}
}
>
<InjectIntl(FormattedDate)
day="2-digit"
month="short"
style={
Object {
"color": "#aaa",
"fontSize": 14,
"fontWeight": "600",
}
}
value={1970-01-18T17:19:12.392Z}
weekday="short"
year="numeric"
/>
</Component>,
<Component
style={
Object {
"backgroundColor": "#aaa",
"flex": 1,
"height": 1,
"opacity": 0.2,
}
}
/>,
],
"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": <InjectIntl(FormattedDate)
day="2-digit"
month="short"
style={
Object {
"color": "#aaa",
"fontSize": 14,
"fontWeight": "600",
}
}
value={1970-01-18T17:19:12.392Z}
weekday="short"
year="numeric"
/>,
"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 [
<Component
style={
Object {
"backgroundColor": "#aaa",
"flex": 1,
"height": 1,
"opacity": 0.2,
}
}
/>,
<Component
style={
Object {
"marginHorizontal": 15,
}
}
>
<InjectIntl(FormattedDate)
day="2-digit"
month="short"
style={
Object {
"color": "#aaa",
"fontSize": 14,
"fontWeight": "600",
}
}
value={1970-01-18T17:19:12.392Z}
weekday="short"
year="numeric"
/>
</Component>,
<Component
style={
Object {
"backgroundColor": "#aaa",
"flex": 1,
"height": 1,
"opacity": 0.2,
}
}
/>,
],
"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": <InjectIntl(FormattedDate)
day="2-digit"
month="short"
style={
Object {
"color": "#aaa",
"fontSize": 14,
"fontWeight": "600",
}
}
value={1970-01-18T17:19:12.392Z}
weekday="short"
year="numeric"
/>,
"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__): <DateHeader
dateLineString="date-1531152392"
index={0}
theme={
Object {
"centerChannelBg": "#aaa",
"centerChannelColor": "#aaa",
}
}
/>,
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 [
<Component
style={
Object {
"backgroundColor": "#aaa",
"flex": 1,
"height": 1,
"opacity": 0.2,
}
}
/>,
<Component
style={
Object {
"marginHorizontal": 15,
}
}
>
<InjectIntl(FormattedDate)
day="2-digit"
month="short"
style={
Object {
"color": "#aaa",
"fontSize": 14,
"fontWeight": "600",
}
}
value={1970-01-18T17:19:12.392Z}
weekday="short"
year="numeric"
/>
</Component>,
<Component
style={
Object {
"backgroundColor": "#aaa",
"flex": 1,
"height": 1,
"opacity": 0.2,
}
}
/>,
],
"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": <InjectIntl(FormattedDate)
day="2-digit"
month="short"
style={
Object {
"color": "#aaa",
"fontSize": 14,
"fontWeight": "600",
}
}
value={1970-01-18T17:19:12.392Z}
weekday="short"
year="numeric"
/>,
"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 [
<Component
style={
Object {
"backgroundColor": "#aaa",
"flex": 1,
"height": 1,
"opacity": 0.2,
}
}
/>,
<Component
style={
Object {
"marginHorizontal": 15,
}
}
>
<InjectIntl(FormattedDate)
day="2-digit"
month="short"
style={
Object {
"color": "#aaa",
"fontSize": 14,
"fontWeight": "600",
}
}
value={1970-01-18T17:19:12.392Z}
weekday="short"
year="numeric"
/>
</Component>,
<Component
style={
Object {
"backgroundColor": "#aaa",
"flex": 1,
"height": 1,
"opacity": 0.2,
}
}
/>,
],
"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": <InjectIntl(FormattedDate)
day="2-digit"
month="short"
style={
Object {
"color": "#aaa",
"fontSize": 14,
"fontWeight": "600",
}
}
value={1970-01-18T17:19:12.392Z}
weekday="short"
year="numeric"
/>,
"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],
},
},
},
}
`;

View file

@ -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 (
<View style={[style.container, this.props.style]}>

View file

@ -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(
<DateHeader {...props}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper).toMatchSnapshot();
});
it('with suffix', () => {
const props = {
...baseProps,
dateLineString: 'date-1531152392-index-2',
index: 2,
};
const wrapper = shallow(
<DateHeader {...props}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper).toMatchSnapshot();
});
});
});

View file

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

View file

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

View file

@ -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 (
<DateHeader
dateLineString={item}
index={index}
/>
);
}
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 (
<DateHeader
date={date}
index={index}
/>
);
};
renderPost = (postId, previousPostId, nextPostId, index) => {
const {
highlightPostId,

View file

@ -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 (
<DateHeader
key={`${date}-${index}`}
date={date}
dateLineString={item}
index={index}
/>
);
@ -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 = <PostSeparator theme={theme}/>;
}

View file

@ -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 (
<DateHeader
key={`${date}-${index}`}
date={date}
dateLineString={item}
index={index}
/>
);
@ -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 = <PostSeparator theme={theme}/>;
}

View file

@ -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 (
<DateHeader
key={`${date}-${index}`}
date={date}
index={index}
/>
);
};
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 (
<DateHeader
dateLineString={item}
index={index}
/>
);
}
let separator;
const nextPost = postIds[index + 1];
if (nextPost && nextPost.indexOf(DATE_LINE) === -1) {
if (nextPost && !isDateLine(nextPost)) {
separator = <PostSeparator theme={theme}/>;
}

View file

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

View file

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