From 6832b8accaffc7603b4d453d53b25a0f3ff2b88b Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Fri, 4 Dec 2020 12:41:40 -0800 Subject: [PATCH] Timeline: repair oldest/newest metrics if we fetch nothing --- .eslintrc.js | 2 +- package.json | 4 +- preload.js | 7 +- ts/state/ducks/conversations.ts | 103 +++++ ts/state/selectors/conversations.ts | 13 +- ts/state/selectors/items.ts | 20 + ts/state/selectors/search.ts | 3 +- ts/state/selectors/user.ts | 8 - .../state/ducks/conversations_test.ts | 384 ++++++++++++++++++ .../state/selectors/conversations_test.ts | 40 +- ts/test-both/state/selectors/items_test.ts | 40 ++ .../state/ducks/conversations_test.ts | 118 ------ .../components/LeftPane_test.tsx | 0 .../media-gallery/groupMessagesByDate_test.ts | 0 ts/{test => test-node}/helpers.ts | 0 .../license_comments_test.ts | 0 .../isLinkPreviewDateValid_test.ts | 0 .../quill/emoji/completion_test.tsx | 0 .../quill/memberRepository_test.ts | 0 .../quill/mentions/completion_test.tsx | 0 .../quill/mentions/matchers_test.ts | 0 ts/{test => test-node}/quill/util_test.ts | 0 ts/{test => test-node}/tslint.json | 0 .../types/Attachment_test.ts | 0 ts/{test => test-node}/types/Contact_test.ts | 0 ts/{test => test-node}/types/Settings_test.ts | 0 .../initializeAttachmentMetadata_test.ts | 0 ts/{test => test-node}/updater/common_test.ts | 0 ts/{test => test-node}/updater/curve_test.ts | 0 .../updater/signature_test.ts | 0 .../util/LatestQueue_test.ts | 0 .../util/combineNames_test.ts | 0 .../util/getAnimatedPngDataIfExists_test.ts | 0 ts/{test => test-node}/util/getOwn_test.ts | 0 .../util/getTextWithMentions_test.ts | 0 .../util/getUserAgent_test.ts | 0 .../util/isFileDangerous_test.ts | 0 ts/{test => test-node}/util/isMuted_test.ts | 0 .../util/isPathInside_test.ts | 0 .../nonRenderedRemoteParticipant_test.ts | 0 .../normalizeGroupCallTimestamp_test.ts | 0 ts/{test => test-node}/util/sgnlHref_test.ts | 0 ts/{test => test-node}/util/sleep_test.ts | 0 .../util/sniffImageMimeType_test.ts | 0 .../util/windowsZoneIdentifier_test.ts | 0 ts/util/lint/exceptions.json | 6 +- ts/views/conversation_view.ts | 4 + 47 files changed, 579 insertions(+), 173 deletions(-) create mode 100644 ts/state/selectors/items.ts create mode 100644 ts/test-both/state/ducks/conversations_test.ts rename ts/{test => test-both}/state/selectors/conversations_test.ts (88%) create mode 100644 ts/test-both/state/selectors/items_test.ts delete mode 100644 ts/test-electron/state/ducks/conversations_test.ts rename ts/{test => test-node}/components/LeftPane_test.tsx (100%) rename ts/{test => test-node}/components/media-gallery/groupMessagesByDate_test.ts (100%) rename ts/{test => test-node}/helpers.ts (100%) rename ts/{test => test-node}/license_comments_test.ts (100%) rename ts/{test => test-node}/linkPreviews/isLinkPreviewDateValid_test.ts (100%) rename ts/{test => test-node}/quill/emoji/completion_test.tsx (100%) rename ts/{test => test-node}/quill/memberRepository_test.ts (100%) rename ts/{test => test-node}/quill/mentions/completion_test.tsx (100%) rename ts/{test => test-node}/quill/mentions/matchers_test.ts (100%) rename ts/{test => test-node}/quill/util_test.ts (100%) rename ts/{test => test-node}/tslint.json (100%) rename ts/{test => test-node}/types/Attachment_test.ts (100%) rename ts/{test => test-node}/types/Contact_test.ts (100%) rename ts/{test => test-node}/types/Settings_test.ts (100%) rename ts/{test => test-node}/types/message/initializeAttachmentMetadata_test.ts (100%) rename ts/{test => test-node}/updater/common_test.ts (100%) rename ts/{test => test-node}/updater/curve_test.ts (100%) rename ts/{test => test-node}/updater/signature_test.ts (100%) rename ts/{test => test-node}/util/LatestQueue_test.ts (100%) rename ts/{test => test-node}/util/combineNames_test.ts (100%) rename ts/{test => test-node}/util/getAnimatedPngDataIfExists_test.ts (100%) rename ts/{test => test-node}/util/getOwn_test.ts (100%) rename ts/{test => test-node}/util/getTextWithMentions_test.ts (100%) rename ts/{test => test-node}/util/getUserAgent_test.ts (100%) rename ts/{test => test-node}/util/isFileDangerous_test.ts (100%) rename ts/{test => test-node}/util/isMuted_test.ts (100%) rename ts/{test => test-node}/util/isPathInside_test.ts (100%) rename ts/{test => test-node}/util/ringrtc/nonRenderedRemoteParticipant_test.ts (100%) rename ts/{test => test-node}/util/ringrtc/normalizeGroupCallTimestamp_test.ts (100%) rename ts/{test => test-node}/util/sgnlHref_test.ts (100%) rename ts/{test => test-node}/util/sleep_test.ts (100%) rename ts/{test => test-node}/util/sniffImageMimeType_test.ts (100%) rename ts/{test => test-node}/util/windowsZoneIdentifier_test.ts (100%) diff --git a/.eslintrc.js b/.eslintrc.js index 55286c2244c..1e87a311b7f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -150,7 +150,7 @@ module.exports = { rules, }, { - files: ['**/*.stories.tsx', 'ts/build/**', 'ts/test/**'], + files: ['**/*.stories.tsx', 'ts/build/**', 'ts/test-*/**'], rules: { ...rules, 'import/no-extraneous-dependencies': 'off', diff --git a/package.json b/package.json index cdefdc04905..fd60ece440b 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh", "test": "yarn test-node && yarn test-electron", "test-electron": "yarn grunt test", - "test-node": "electron-mocha --recursive test/app test/modules ts/test", - "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test", + "test-node": "electron-mocha --recursive test/app test/modules ts/test-node ts/test-both", + "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test-node ts/test-both", "eslint": "eslint .", "lint": "yarn format --list-different && yarn eslint", "lint-deps": "node ts/util/lint/linter.js", diff --git a/preload.js b/preload.js index 129f9962c59..7d86b9332d1 100644 --- a/preload.js +++ b/preload.js @@ -563,9 +563,12 @@ try { }; /* eslint-disable global-require, import/no-extraneous-dependencies */ - require('./ts/test-electron/models/messages_test'); + require('./ts/test-both/state/ducks/conversations_test'); + require('./ts/test-both/state/selectors/conversations_test'); + require('./ts/test-both/state/selectors/items_test'); + require('./ts/test-electron/linkPreviews/linkPreviewFetch_test'); - require('./ts/test-electron/state/ducks/conversations_test'); + require('./ts/test-electron/models/messages_test'); require('./ts/test-electron/state/ducks/calling_test'); require('./ts/test-electron/state/selectors/calling_test'); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 5b3adf7f2cd..959f287c24c 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -13,6 +13,8 @@ import { values, without, } from 'lodash'; + +import { getOwn } from '../../util/getOwn'; import { trigger } from '../../shims/events'; import { NoopActionType } from './noop'; import { AttachmentType } from '../../types/Attachment'; @@ -281,6 +283,19 @@ export type MessagesAddedActionType = { isActive: boolean; }; }; + +export type RepairNewestMessageActionType = { + type: 'REPAIR_NEWEST_MESSAGE'; + payload: { + conversationId: string; + }; +}; +export type RepairOldestMessageActionType = { + type: 'REPAIR_OLDEST_MESSAGE'; + payload: { + conversationId: string; + }; +}; export type MessagesResetActionType = { type: 'MESSAGES_RESET'; payload: { @@ -367,6 +382,8 @@ export type ConversationActionType = | MessageChangedActionType | MessageDeletedActionType | MessagesAddedActionType + | RepairNewestMessageActionType + | RepairOldestMessageActionType | MessagesResetActionType | SetMessagesLoadingActionType | SetIsNearBottomActionType @@ -407,6 +424,8 @@ export const actions = { openConversationExternal, showInbox, showArchivedConversations, + repairNewestMessage, + repairOldestMessage, }; function conversationAdded( @@ -511,6 +530,28 @@ function messagesAdded( }, }; } + +function repairNewestMessage( + conversationId: string +): RepairNewestMessageActionType { + return { + type: 'REPAIR_NEWEST_MESSAGE', + payload: { + conversationId, + }, + }; +} +function repairOldestMessage( + conversationId: string +): RepairOldestMessageActionType { + return { + type: 'REPAIR_OLDEST_MESSAGE', + payload: { + conversationId, + }, + }; +} + function messagesReset( conversationId: string, messages: Array, @@ -1119,6 +1160,68 @@ export function reducer( }, }; } + + if (action.type === 'REPAIR_NEWEST_MESSAGE') { + const { conversationId } = action.payload; + const { messagesByConversation, messagesLookup } = state; + + const existingConversation = getOwn(messagesByConversation, conversationId); + if (!existingConversation) { + return state; + } + + const { messageIds } = existingConversation; + const lastId = + messageIds && messageIds.length + ? messageIds[messageIds.length - 1] + : undefined; + const last = lastId ? getOwn(messagesLookup, lastId) : undefined; + const newest = last ? pick(last, ['id', 'received_at']) : undefined; + + return { + ...state, + messagesByConversation: { + ...messagesByConversation, + [conversationId]: { + ...existingConversation, + metrics: { + ...existingConversation.metrics, + newest, + }, + }, + }, + }; + } + + if (action.type === 'REPAIR_OLDEST_MESSAGE') { + const { conversationId } = action.payload; + const { messagesByConversation, messagesLookup } = state; + + const existingConversation = getOwn(messagesByConversation, conversationId); + if (!existingConversation) { + return state; + } + + const { messageIds } = existingConversation; + const firstId = messageIds && messageIds.length ? messageIds[0] : undefined; + const first = firstId ? getOwn(messagesLookup, firstId) : undefined; + const oldest = first ? pick(first, ['id', 'received_at']) : undefined; + + return { + ...state, + messagesByConversation: { + ...messagesByConversation, + [conversationId]: { + ...existingConversation, + metrics: { + ...existingConversation.metrics, + oldest, + }, + }, + }, + }; + } + if (action.type === 'MESSAGES_ADDED') { const { conversationId, isActive, isNewMessage, messages } = action.payload; const { messagesByConversation, messagesLookup } = state; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 900bd1b55e5..612987b895b 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -15,6 +15,7 @@ import { MessagesByConversationType, MessageType, } from '../ducks/conversations'; + import { getBubbleProps } from '../../shims/Whisper'; import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline'; import { TimelineItemType } from '../../components/conversation/TimelineItem'; @@ -26,6 +27,7 @@ import { getUserConversationId, getUserNumber, } from './user'; +import { getPinnedConversationIds } from './items'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -127,7 +129,8 @@ export const getConversationComparator = createSelector( export const _getLeftPaneLists = ( lookup: ConversationLookupType, comparator: (left: ConversationType, right: ConversationType) => number, - selectedConversation?: string + selectedConversation?: string, + pinnedConversationIds?: Array ): { conversations: Array; archivedConversations: Array; @@ -162,13 +165,10 @@ export const _getLeftPaneLists = ( conversations.sort(comparator); archivedConversations.sort(comparator); - const pinnedConversationIds = window.storage.get>( - 'pinnedConversationIds', - [] - ); pinnedConversations.sort( (a, b) => - pinnedConversationIds.indexOf(a.id) - pinnedConversationIds.indexOf(b.id) + (pinnedConversationIds || []).indexOf(a.id) - + (pinnedConversationIds || []).indexOf(b.id) ); return { conversations, archivedConversations, pinnedConversations }; @@ -178,6 +178,7 @@ export const getLeftPaneLists = createSelector( getConversationLookup, getConversationComparator, getSelectedConversation, + getPinnedConversationIds, _getLeftPaneLists ); diff --git a/ts/state/selectors/items.ts b/ts/state/selectors/items.ts new file mode 100644 index 00000000000..bf84b2eff47 --- /dev/null +++ b/ts/state/selectors/items.ts @@ -0,0 +1,20 @@ +// Copyright 2019-2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { createSelector } from 'reselect'; + +import { StateType } from '../reducer'; +import { ItemsStateType } from '../ducks/items'; + +export const getItems = (state: StateType): ItemsStateType => state.items; + +export const getUserAgent = createSelector( + getItems, + (state: ItemsStateType): string => state.userAgent as string +); + +export const getPinnedConversationIds = createSelector( + getItems, + (state: ItemsStateType): Array => + (state.pinnedConversationIds || []) as Array +); diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index be58cf552f7..5a98ebf1c7d 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -24,7 +24,8 @@ import { } from '../../components/SearchResults'; import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/MessageSearchResult'; -import { getRegionCode, getUserAgent, getUserNumber } from './user'; +import { getRegionCode, getUserNumber } from './user'; +import { getUserAgent } from './items'; import { GetConversationByIdType, getConversationLookup, diff --git a/ts/state/selectors/user.ts b/ts/state/selectors/user.ts index 4823d5dac0c..253a91faca9 100644 --- a/ts/state/selectors/user.ts +++ b/ts/state/selectors/user.ts @@ -7,12 +7,9 @@ import { LocalizerType } from '../../types/Util'; import { StateType } from '../reducer'; import { UserStateType } from '../ducks/user'; -import { ItemsStateType } from '../ducks/items'; export const getUser = (state: StateType): UserStateType => state.user; -export const getItems = (state: StateType): ItemsStateType => state.items; - export const getUserNumber = createSelector( getUser, (state: UserStateType): string => state.ourNumber @@ -33,11 +30,6 @@ export const getUserUuid = createSelector( (state: UserStateType): string => state.ourUuid ); -export const getUserAgent = createSelector( - getItems, - (state: ItemsStateType): string => state.userAgent as string -); - export const getIntl = createSelector( getUser, (state: UserStateType): LocalizerType => state.i18n diff --git a/ts/test-both/state/ducks/conversations_test.ts b/ts/test-both/state/ducks/conversations_test.ts new file mode 100644 index 00000000000..d9d41482eff --- /dev/null +++ b/ts/test-both/state/ducks/conversations_test.ts @@ -0,0 +1,384 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import { + actions, + ConversationMessageType, + ConversationsStateType, + ConversationType, + getConversationCallMode, + MessageType, + reducer, +} from '../../../state/ducks/conversations'; +import { CallMode } from '../../../types/Calling'; + +const { repairNewestMessage, repairOldestMessage } = actions; + +describe('both/state/ducks/conversations', () => { + describe('helpers', () => { + describe('getConversationCallMode', () => { + const fakeConversation: ConversationType = { + id: 'id1', + e164: '+18005551111', + activeAt: Date.now(), + name: 'No timestamp', + timestamp: 0, + inboxPosition: 0, + phoneNumber: 'notused', + isArchived: false, + markedUnread: false, + + type: 'direct', + isMe: false, + lastUpdated: Date.now(), + title: 'No timestamp', + unreadCount: 1, + isSelected: false, + typingContact: { + name: 'Someone There', + color: 'blue', + phoneNumber: '+18005551111', + }, + + acceptedMessageRequest: true, + }; + + it("returns CallMode.None if you've left the conversation", () => { + assert.strictEqual( + getConversationCallMode({ + ...fakeConversation, + left: true, + }), + CallMode.None + ); + }); + + it("returns CallMode.None if you've blocked the other person", () => { + assert.strictEqual( + getConversationCallMode({ + ...fakeConversation, + isBlocked: true, + }), + CallMode.None + ); + }); + + it("returns CallMode.None if you haven't accepted message requests", () => { + assert.strictEqual( + getConversationCallMode({ + ...fakeConversation, + acceptedMessageRequest: false, + }), + CallMode.None + ); + }); + + it('returns CallMode.None if the conversation is Note to Self', () => { + assert.strictEqual( + getConversationCallMode({ + ...fakeConversation, + isMe: true, + }), + CallMode.None + ); + }); + + it('returns CallMode.None for v1 groups', () => { + assert.strictEqual( + getConversationCallMode({ + ...fakeConversation, + type: 'group', + groupVersion: 1, + }), + CallMode.None + ); + + assert.strictEqual( + getConversationCallMode({ + ...fakeConversation, + type: 'group', + }), + CallMode.None + ); + }); + + it('returns CallMode.Direct if the conversation is a normal direct conversation', () => { + assert.strictEqual( + getConversationCallMode(fakeConversation), + CallMode.Direct + ); + }); + + it('returns CallMode.Group if the conversation is a v2 group', () => { + assert.strictEqual( + getConversationCallMode({ + ...fakeConversation, + type: 'group', + groupVersion: 2, + }), + CallMode.Group + ); + }); + }); + }); + + describe('reducer', () => { + const time = Date.now(); + const conversationId = 'conversation-guid-1'; + const messageId = 'message-guid-1'; + const messageIdTwo = 'message-guid-2'; + const messageIdThree = 'message-guid-3'; + + function getDefaultState(): ConversationsStateType { + return { + conversationLookup: {}, + selectedMessageCounter: 0, + selectedConversationPanelDepth: 0, + showArchived: false, + messagesLookup: {}, + messagesByConversation: {}, + }; + } + + function getDefaultMessage(id: string): MessageType { + return { + id, + conversationId: 'conversationId', + source: 'source', + type: 'incoming' as const, + received_at: Date.now(), + attachments: [], + sticker: {}, + unread: false, + }; + } + + function getDefaultConversationMessage(): ConversationMessageType { + return { + heightChangeMessageIds: [], + isLoadingMessages: false, + messageIds: [], + metrics: { + totalUnread: 0, + }, + resetCounter: 0, + scrollToMessageCounter: 0, + }; + } + + describe('REPAIR_NEWEST_MESSAGE', () => { + it('updates newest', () => { + const action = repairNewestMessage(conversationId); + const state: ConversationsStateType = { + ...getDefaultState(), + messagesLookup: { + [messageId]: { + ...getDefaultMessage(messageId), + received_at: time, + }, + }, + messagesByConversation: { + [conversationId]: { + ...getDefaultConversationMessage(), + messageIds: [messageIdThree, messageIdTwo, messageId], + metrics: { + totalUnread: 0, + }, + }, + }, + }; + + const expected: ConversationsStateType = { + ...getDefaultState(), + messagesLookup: { + [messageId]: { + ...getDefaultMessage(messageId), + received_at: time, + }, + }, + messagesByConversation: { + [conversationId]: { + ...getDefaultConversationMessage(), + messageIds: [messageIdThree, messageIdTwo, messageId], + metrics: { + totalUnread: 0, + newest: { + id: messageId, + received_at: time, + }, + }, + }, + }, + }; + + const actual = reducer(state, action); + assert.deepEqual(actual, expected); + }); + + it('clears newest', () => { + const action = repairNewestMessage(conversationId); + const state: ConversationsStateType = { + ...getDefaultState(), + messagesLookup: { + [messageId]: { + ...getDefaultMessage(messageId), + received_at: time, + }, + }, + messagesByConversation: { + [conversationId]: { + ...getDefaultConversationMessage(), + messageIds: [], + metrics: { + totalUnread: 0, + newest: { + id: messageId, + received_at: time, + }, + }, + }, + }, + }; + + const expected: ConversationsStateType = { + ...getDefaultState(), + messagesLookup: { + [messageId]: { + ...getDefaultMessage(messageId), + received_at: time, + }, + }, + messagesByConversation: { + [conversationId]: { + ...getDefaultConversationMessage(), + messageIds: [], + metrics: { + newest: undefined, + totalUnread: 0, + }, + }, + }, + }; + + const actual = reducer(state, action); + assert.deepEqual(actual, expected); + }); + + it('returns state if conversation not present', () => { + const action = repairNewestMessage(conversationId); + const state: ConversationsStateType = getDefaultState(); + const actual = reducer(state, action); + + assert.equal(actual, state); + }); + }); + + describe('REPAIR_OLDEST_MESSAGE', () => { + it('updates oldest', () => { + const action = repairOldestMessage(conversationId); + const state: ConversationsStateType = { + ...getDefaultState(), + messagesLookup: { + [messageId]: { + ...getDefaultMessage(messageId), + received_at: time, + }, + }, + messagesByConversation: { + [conversationId]: { + ...getDefaultConversationMessage(), + messageIds: [messageId, messageIdTwo, messageIdThree], + metrics: { + totalUnread: 0, + }, + }, + }, + }; + + const expected: ConversationsStateType = { + ...getDefaultState(), + messagesLookup: { + [messageId]: { + ...getDefaultMessage(messageId), + received_at: time, + }, + }, + messagesByConversation: { + [conversationId]: { + ...getDefaultConversationMessage(), + messageIds: [messageId, messageIdTwo, messageIdThree], + metrics: { + totalUnread: 0, + oldest: { + id: messageId, + received_at: time, + }, + }, + }, + }, + }; + + const actual = reducer(state, action); + assert.deepEqual(actual, expected); + }); + + it('clears oldest', () => { + const action = repairOldestMessage(conversationId); + const state: ConversationsStateType = { + ...getDefaultState(), + messagesLookup: { + [messageId]: { + ...getDefaultMessage(messageId), + received_at: time, + }, + }, + messagesByConversation: { + [conversationId]: { + ...getDefaultConversationMessage(), + messageIds: [], + metrics: { + totalUnread: 0, + oldest: { + id: messageId, + received_at: time, + }, + }, + }, + }, + }; + + const expected: ConversationsStateType = { + ...getDefaultState(), + messagesLookup: { + [messageId]: { + ...getDefaultMessage(messageId), + received_at: time, + }, + }, + messagesByConversation: { + [conversationId]: { + ...getDefaultConversationMessage(), + messageIds: [], + metrics: { + oldest: undefined, + totalUnread: 0, + }, + }, + }, + }; + + const actual = reducer(state, action); + assert.deepEqual(actual, expected); + }); + + it('returns state if conversation not present', () => { + const action = repairOldestMessage(conversationId); + const state: ConversationsStateType = getDefaultState(); + const actual = reducer(state, action); + + assert.equal(actual, state); + }); + }); + }); +}); diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test-both/state/selectors/conversations_test.ts similarity index 88% rename from ts/test/state/selectors/conversations_test.ts rename to ts/test-both/state/selectors/conversations_test.ts index 64c213e740c..b673f91986a 100644 --- a/ts/test/state/selectors/conversations_test.ts +++ b/ts/test-both/state/selectors/conversations_test.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; -import * as sinon from 'sinon'; import { ConversationLookupType } from '../../../state/ducks/conversations'; import { @@ -10,28 +9,7 @@ import { _getLeftPaneLists, } from '../../../state/selectors/conversations'; -describe('state/selectors/conversations', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const globalAsAny = global as any; - - beforeEach(function beforeEach() { - this.oldWindow = globalAsAny.window; - globalAsAny.window = {}; - - window.storage = { - get: sinon.stub().returns([]), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - }); - - afterEach(function afterEach() { - if (this.oldWindow === undefined) { - delete globalAsAny.window; - } else { - globalAsAny.window = this.oldWindow; - } - }); - +describe('both/state/selectors/conversations', () => { describe('#getLeftPaneList', () => { it('sorts conversations based on timestamp then by intl-friendly title', () => { const data: ConversationLookupType = { @@ -172,14 +150,6 @@ describe('state/selectors/conversations', () => { }); describe('given pinned conversations', () => { - beforeEach(() => { - (window.storage.get as sinon.SinonStub).returns([ - 'pin1', - 'pin2', - 'pin3', - ]); - }); - it('sorts pinned conversations based on order in storage', () => { const data: ConversationLookupType = { pin2: { @@ -262,8 +232,14 @@ describe('state/selectors/conversations', () => { }, }; + const pinnedConversationIds = ['pin1', 'pin2', 'pin3']; const comparator = _getConversationComparator(); - const { pinnedConversations } = _getLeftPaneLists(data, comparator); + const { pinnedConversations } = _getLeftPaneLists( + data, + comparator, + undefined, + pinnedConversationIds + ); assert.strictEqual(pinnedConversations[0].name, 'Pin One'); assert.strictEqual(pinnedConversations[1].name, 'Pin Two'); diff --git a/ts/test-both/state/selectors/items_test.ts b/ts/test-both/state/selectors/items_test.ts new file mode 100644 index 00000000000..59b885630cb --- /dev/null +++ b/ts/test-both/state/selectors/items_test.ts @@ -0,0 +1,40 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import { getPinnedConversationIds } from '../../../state/selectors/items'; +import type { StateType } from '../../../state/reducer'; + +describe('both/state/selectors/items', () => { + describe('#getPinnedConversationIds', () => { + // Note: we would like to use the full reducer here, to get a real empty state object + // but we cannot load the full reducer inside of electron-mocha. + function getDefaultStateType(): StateType { + return { + items: {}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + } + + it('returns pinnedConversationIds key from items', () => { + const expected = ['one', 'two']; + const state: StateType = { + ...getDefaultStateType(), + items: { + pinnedConversationIds: expected, + }, + }; + + const actual = getPinnedConversationIds(state); + assert.deepEqual(actual, expected); + }); + + it('returns empty array if no saved data', () => { + const expected: Array = []; + const state = getDefaultStateType(); + + const actual = getPinnedConversationIds(state); + assert.deepEqual(actual, expected); + }); + }); +}); diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts deleted file mode 100644 index 70abd25aadd..00000000000 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { assert } from 'chai'; -import { - getConversationCallMode, - ConversationType, -} from '../../../state/ducks/conversations'; -import { CallMode } from '../../../types/Calling'; - -describe('conversations duck', () => { - describe('helpers', () => { - describe('getConversationCallMode', () => { - const fakeConversation: ConversationType = { - id: 'id1', - e164: '+18005551111', - activeAt: Date.now(), - name: 'No timestamp', - timestamp: 0, - inboxPosition: 0, - phoneNumber: 'notused', - isArchived: false, - markedUnread: false, - - type: 'direct', - isMe: false, - lastUpdated: Date.now(), - title: 'No timestamp', - unreadCount: 1, - isSelected: false, - typingContact: { - name: 'Someone There', - color: 'blue', - phoneNumber: '+18005551111', - }, - - acceptedMessageRequest: true, - }; - - it("returns CallMode.None if you've left the conversation", () => { - assert.strictEqual( - getConversationCallMode({ - ...fakeConversation, - left: true, - }), - CallMode.None - ); - }); - - it("returns CallMode.None if you've blocked the other person", () => { - assert.strictEqual( - getConversationCallMode({ - ...fakeConversation, - isBlocked: true, - }), - CallMode.None - ); - }); - - it("returns CallMode.None if you haven't accepted message requests", () => { - assert.strictEqual( - getConversationCallMode({ - ...fakeConversation, - acceptedMessageRequest: false, - }), - CallMode.None - ); - }); - - it('returns CallMode.None if the conversation is Note to Self', () => { - assert.strictEqual( - getConversationCallMode({ - ...fakeConversation, - isMe: true, - }), - CallMode.None - ); - }); - - it('returns CallMode.None for v1 groups', () => { - assert.strictEqual( - getConversationCallMode({ - ...fakeConversation, - type: 'group', - groupVersion: 1, - }), - CallMode.None - ); - - assert.strictEqual( - getConversationCallMode({ - ...fakeConversation, - type: 'group', - }), - CallMode.None - ); - }); - - it('returns CallMode.Direct if the conversation is a normal direct conversation', () => { - assert.strictEqual( - getConversationCallMode(fakeConversation), - CallMode.Direct - ); - }); - - it('returns CallMode.Group if the conversation is a v2 group', () => { - assert.strictEqual( - getConversationCallMode({ - ...fakeConversation, - type: 'group', - groupVersion: 2, - }), - CallMode.Group - ); - }); - }); - }); -}); diff --git a/ts/test/components/LeftPane_test.tsx b/ts/test-node/components/LeftPane_test.tsx similarity index 100% rename from ts/test/components/LeftPane_test.tsx rename to ts/test-node/components/LeftPane_test.tsx diff --git a/ts/test/components/media-gallery/groupMessagesByDate_test.ts b/ts/test-node/components/media-gallery/groupMessagesByDate_test.ts similarity index 100% rename from ts/test/components/media-gallery/groupMessagesByDate_test.ts rename to ts/test-node/components/media-gallery/groupMessagesByDate_test.ts diff --git a/ts/test/helpers.ts b/ts/test-node/helpers.ts similarity index 100% rename from ts/test/helpers.ts rename to ts/test-node/helpers.ts diff --git a/ts/test/license_comments_test.ts b/ts/test-node/license_comments_test.ts similarity index 100% rename from ts/test/license_comments_test.ts rename to ts/test-node/license_comments_test.ts diff --git a/ts/test/linkPreviews/isLinkPreviewDateValid_test.ts b/ts/test-node/linkPreviews/isLinkPreviewDateValid_test.ts similarity index 100% rename from ts/test/linkPreviews/isLinkPreviewDateValid_test.ts rename to ts/test-node/linkPreviews/isLinkPreviewDateValid_test.ts diff --git a/ts/test/quill/emoji/completion_test.tsx b/ts/test-node/quill/emoji/completion_test.tsx similarity index 100% rename from ts/test/quill/emoji/completion_test.tsx rename to ts/test-node/quill/emoji/completion_test.tsx diff --git a/ts/test/quill/memberRepository_test.ts b/ts/test-node/quill/memberRepository_test.ts similarity index 100% rename from ts/test/quill/memberRepository_test.ts rename to ts/test-node/quill/memberRepository_test.ts diff --git a/ts/test/quill/mentions/completion_test.tsx b/ts/test-node/quill/mentions/completion_test.tsx similarity index 100% rename from ts/test/quill/mentions/completion_test.tsx rename to ts/test-node/quill/mentions/completion_test.tsx diff --git a/ts/test/quill/mentions/matchers_test.ts b/ts/test-node/quill/mentions/matchers_test.ts similarity index 100% rename from ts/test/quill/mentions/matchers_test.ts rename to ts/test-node/quill/mentions/matchers_test.ts diff --git a/ts/test/quill/util_test.ts b/ts/test-node/quill/util_test.ts similarity index 100% rename from ts/test/quill/util_test.ts rename to ts/test-node/quill/util_test.ts diff --git a/ts/test/tslint.json b/ts/test-node/tslint.json similarity index 100% rename from ts/test/tslint.json rename to ts/test-node/tslint.json diff --git a/ts/test/types/Attachment_test.ts b/ts/test-node/types/Attachment_test.ts similarity index 100% rename from ts/test/types/Attachment_test.ts rename to ts/test-node/types/Attachment_test.ts diff --git a/ts/test/types/Contact_test.ts b/ts/test-node/types/Contact_test.ts similarity index 100% rename from ts/test/types/Contact_test.ts rename to ts/test-node/types/Contact_test.ts diff --git a/ts/test/types/Settings_test.ts b/ts/test-node/types/Settings_test.ts similarity index 100% rename from ts/test/types/Settings_test.ts rename to ts/test-node/types/Settings_test.ts diff --git a/ts/test/types/message/initializeAttachmentMetadata_test.ts b/ts/test-node/types/message/initializeAttachmentMetadata_test.ts similarity index 100% rename from ts/test/types/message/initializeAttachmentMetadata_test.ts rename to ts/test-node/types/message/initializeAttachmentMetadata_test.ts diff --git a/ts/test/updater/common_test.ts b/ts/test-node/updater/common_test.ts similarity index 100% rename from ts/test/updater/common_test.ts rename to ts/test-node/updater/common_test.ts diff --git a/ts/test/updater/curve_test.ts b/ts/test-node/updater/curve_test.ts similarity index 100% rename from ts/test/updater/curve_test.ts rename to ts/test-node/updater/curve_test.ts diff --git a/ts/test/updater/signature_test.ts b/ts/test-node/updater/signature_test.ts similarity index 100% rename from ts/test/updater/signature_test.ts rename to ts/test-node/updater/signature_test.ts diff --git a/ts/test/util/LatestQueue_test.ts b/ts/test-node/util/LatestQueue_test.ts similarity index 100% rename from ts/test/util/LatestQueue_test.ts rename to ts/test-node/util/LatestQueue_test.ts diff --git a/ts/test/util/combineNames_test.ts b/ts/test-node/util/combineNames_test.ts similarity index 100% rename from ts/test/util/combineNames_test.ts rename to ts/test-node/util/combineNames_test.ts diff --git a/ts/test/util/getAnimatedPngDataIfExists_test.ts b/ts/test-node/util/getAnimatedPngDataIfExists_test.ts similarity index 100% rename from ts/test/util/getAnimatedPngDataIfExists_test.ts rename to ts/test-node/util/getAnimatedPngDataIfExists_test.ts diff --git a/ts/test/util/getOwn_test.ts b/ts/test-node/util/getOwn_test.ts similarity index 100% rename from ts/test/util/getOwn_test.ts rename to ts/test-node/util/getOwn_test.ts diff --git a/ts/test/util/getTextWithMentions_test.ts b/ts/test-node/util/getTextWithMentions_test.ts similarity index 100% rename from ts/test/util/getTextWithMentions_test.ts rename to ts/test-node/util/getTextWithMentions_test.ts diff --git a/ts/test/util/getUserAgent_test.ts b/ts/test-node/util/getUserAgent_test.ts similarity index 100% rename from ts/test/util/getUserAgent_test.ts rename to ts/test-node/util/getUserAgent_test.ts diff --git a/ts/test/util/isFileDangerous_test.ts b/ts/test-node/util/isFileDangerous_test.ts similarity index 100% rename from ts/test/util/isFileDangerous_test.ts rename to ts/test-node/util/isFileDangerous_test.ts diff --git a/ts/test/util/isMuted_test.ts b/ts/test-node/util/isMuted_test.ts similarity index 100% rename from ts/test/util/isMuted_test.ts rename to ts/test-node/util/isMuted_test.ts diff --git a/ts/test/util/isPathInside_test.ts b/ts/test-node/util/isPathInside_test.ts similarity index 100% rename from ts/test/util/isPathInside_test.ts rename to ts/test-node/util/isPathInside_test.ts diff --git a/ts/test/util/ringrtc/nonRenderedRemoteParticipant_test.ts b/ts/test-node/util/ringrtc/nonRenderedRemoteParticipant_test.ts similarity index 100% rename from ts/test/util/ringrtc/nonRenderedRemoteParticipant_test.ts rename to ts/test-node/util/ringrtc/nonRenderedRemoteParticipant_test.ts diff --git a/ts/test/util/ringrtc/normalizeGroupCallTimestamp_test.ts b/ts/test-node/util/ringrtc/normalizeGroupCallTimestamp_test.ts similarity index 100% rename from ts/test/util/ringrtc/normalizeGroupCallTimestamp_test.ts rename to ts/test-node/util/ringrtc/normalizeGroupCallTimestamp_test.ts diff --git a/ts/test/util/sgnlHref_test.ts b/ts/test-node/util/sgnlHref_test.ts similarity index 100% rename from ts/test/util/sgnlHref_test.ts rename to ts/test-node/util/sgnlHref_test.ts diff --git a/ts/test/util/sleep_test.ts b/ts/test-node/util/sleep_test.ts similarity index 100% rename from ts/test/util/sleep_test.ts rename to ts/test-node/util/sleep_test.ts diff --git a/ts/test/util/sniffImageMimeType_test.ts b/ts/test-node/util/sniffImageMimeType_test.ts similarity index 100% rename from ts/test/util/sniffImageMimeType_test.ts rename to ts/test-node/util/sniffImageMimeType_test.ts diff --git a/ts/test/util/windowsZoneIdentifier_test.ts b/ts/test-node/util/windowsZoneIdentifier_test.ts similarity index 100% rename from ts/test/util/windowsZoneIdentifier_test.ts rename to ts/test-node/util/windowsZoneIdentifier_test.ts diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 91eb9f2747c..6ee5df88880 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -15007,7 +15007,7 @@ }, { "rule": "jQuery-before(", - "path": "ts/test/util/windowsZoneIdentifier_test.js", + "path": "ts/test-node/util/windowsZoneIdentifier_test.js", "line": " before(function thisNeeded() {", "lineNumber": 21, "reasonCategory": "testCode", @@ -15016,7 +15016,7 @@ }, { "rule": "jQuery-before(", - "path": "ts/test/util/windowsZoneIdentifier_test.ts", + "path": "ts/test-node/util/windowsZoneIdentifier_test.ts", "line": " before(function thisNeeded() {", "lineNumber": 15, "reasonCategory": "testCode", @@ -15167,4 +15167,4 @@ "reasonCategory": "falseMatch", "updated": "2020-09-08T23:07:22.682Z" } -] +] \ No newline at end of file diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 0b58076f44e..e661f5bc858 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -823,6 +823,7 @@ Whisper.ConversationView = Whisper.View.extend({ const { messagesAdded, setMessagesLoading, + repairOldestMessage, } = window.reduxActions.conversations; const conversationId = this.model.id; @@ -851,6 +852,7 @@ Whisper.ConversationView = Whisper.View.extend({ window.log.warn( 'loadOlderMessages: requested, but loaded no messages' ); + repairOldestMessage(conversationId); return; } @@ -875,6 +877,7 @@ Whisper.ConversationView = Whisper.View.extend({ const { messagesAdded, setMessagesLoading, + repairNewestMessage, } = window.reduxActions.conversations; const conversationId = this.model.id; @@ -902,6 +905,7 @@ Whisper.ConversationView = Whisper.View.extend({ window.log.warn( 'loadNewerMessages: requested, but loaded no messages' ); + repairNewestMessage(conversationId); return; }