Timeline: repair oldest/newest metrics if we fetch nothing
This commit is contained in:
parent
56ae4a41eb
commit
6832b8acca
47 changed files with 579 additions and 173 deletions
|
@ -150,7 +150,7 @@ module.exports = {
|
||||||
rules,
|
rules,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.stories.tsx', 'ts/build/**', 'ts/test/**'],
|
files: ['**/*.stories.tsx', 'ts/build/**', 'ts/test-*/**'],
|
||||||
rules: {
|
rules: {
|
||||||
...rules,
|
...rules,
|
||||||
'import/no-extraneous-dependencies': 'off',
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
|
|
@ -29,8 +29,8 @@
|
||||||
"publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh",
|
"publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh",
|
||||||
"test": "yarn test-node && yarn test-electron",
|
"test": "yarn test-node && yarn test-electron",
|
||||||
"test-electron": "yarn grunt test",
|
"test-electron": "yarn grunt test",
|
||||||
"test-node": "electron-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",
|
"test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test-node ts/test-both",
|
||||||
"eslint": "eslint .",
|
"eslint": "eslint .",
|
||||||
"lint": "yarn format --list-different && yarn eslint",
|
"lint": "yarn format --list-different && yarn eslint",
|
||||||
"lint-deps": "node ts/util/lint/linter.js",
|
"lint-deps": "node ts/util/lint/linter.js",
|
||||||
|
|
|
@ -563,9 +563,12 @@ try {
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable global-require, import/no-extraneous-dependencies */
|
/* 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/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/ducks/calling_test');
|
||||||
require('./ts/test-electron/state/selectors/calling_test');
|
require('./ts/test-electron/state/selectors/calling_test');
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
values,
|
values,
|
||||||
without,
|
without,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
|
||||||
|
import { getOwn } from '../../util/getOwn';
|
||||||
import { trigger } from '../../shims/events';
|
import { trigger } from '../../shims/events';
|
||||||
import { NoopActionType } from './noop';
|
import { NoopActionType } from './noop';
|
||||||
import { AttachmentType } from '../../types/Attachment';
|
import { AttachmentType } from '../../types/Attachment';
|
||||||
|
@ -281,6 +283,19 @@ export type MessagesAddedActionType = {
|
||||||
isActive: boolean;
|
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 = {
|
export type MessagesResetActionType = {
|
||||||
type: 'MESSAGES_RESET';
|
type: 'MESSAGES_RESET';
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -367,6 +382,8 @@ export type ConversationActionType =
|
||||||
| MessageChangedActionType
|
| MessageChangedActionType
|
||||||
| MessageDeletedActionType
|
| MessageDeletedActionType
|
||||||
| MessagesAddedActionType
|
| MessagesAddedActionType
|
||||||
|
| RepairNewestMessageActionType
|
||||||
|
| RepairOldestMessageActionType
|
||||||
| MessagesResetActionType
|
| MessagesResetActionType
|
||||||
| SetMessagesLoadingActionType
|
| SetMessagesLoadingActionType
|
||||||
| SetIsNearBottomActionType
|
| SetIsNearBottomActionType
|
||||||
|
@ -407,6 +424,8 @@ export const actions = {
|
||||||
openConversationExternal,
|
openConversationExternal,
|
||||||
showInbox,
|
showInbox,
|
||||||
showArchivedConversations,
|
showArchivedConversations,
|
||||||
|
repairNewestMessage,
|
||||||
|
repairOldestMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
function conversationAdded(
|
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(
|
function messagesReset(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
messages: Array<MessageType>,
|
messages: Array<MessageType>,
|
||||||
|
@ -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') {
|
if (action.type === 'MESSAGES_ADDED') {
|
||||||
const { conversationId, isActive, isNewMessage, messages } = action.payload;
|
const { conversationId, isActive, isNewMessage, messages } = action.payload;
|
||||||
const { messagesByConversation, messagesLookup } = state;
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
MessagesByConversationType,
|
MessagesByConversationType,
|
||||||
MessageType,
|
MessageType,
|
||||||
} from '../ducks/conversations';
|
} from '../ducks/conversations';
|
||||||
|
|
||||||
import { getBubbleProps } from '../../shims/Whisper';
|
import { getBubbleProps } from '../../shims/Whisper';
|
||||||
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
||||||
import { TimelineItemType } from '../../components/conversation/TimelineItem';
|
import { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||||
|
@ -26,6 +27,7 @@ import {
|
||||||
getUserConversationId,
|
getUserConversationId,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
} from './user';
|
} from './user';
|
||||||
|
import { getPinnedConversationIds } from './items';
|
||||||
|
|
||||||
export const getConversations = (state: StateType): ConversationsStateType =>
|
export const getConversations = (state: StateType): ConversationsStateType =>
|
||||||
state.conversations;
|
state.conversations;
|
||||||
|
@ -127,7 +129,8 @@ export const getConversationComparator = createSelector(
|
||||||
export const _getLeftPaneLists = (
|
export const _getLeftPaneLists = (
|
||||||
lookup: ConversationLookupType,
|
lookup: ConversationLookupType,
|
||||||
comparator: (left: ConversationType, right: ConversationType) => number,
|
comparator: (left: ConversationType, right: ConversationType) => number,
|
||||||
selectedConversation?: string
|
selectedConversation?: string,
|
||||||
|
pinnedConversationIds?: Array<string>
|
||||||
): {
|
): {
|
||||||
conversations: Array<ConversationType>;
|
conversations: Array<ConversationType>;
|
||||||
archivedConversations: Array<ConversationType>;
|
archivedConversations: Array<ConversationType>;
|
||||||
|
@ -162,13 +165,10 @@ export const _getLeftPaneLists = (
|
||||||
conversations.sort(comparator);
|
conversations.sort(comparator);
|
||||||
archivedConversations.sort(comparator);
|
archivedConversations.sort(comparator);
|
||||||
|
|
||||||
const pinnedConversationIds = window.storage.get<Array<string>>(
|
|
||||||
'pinnedConversationIds',
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
pinnedConversations.sort(
|
pinnedConversations.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
pinnedConversationIds.indexOf(a.id) - pinnedConversationIds.indexOf(b.id)
|
(pinnedConversationIds || []).indexOf(a.id) -
|
||||||
|
(pinnedConversationIds || []).indexOf(b.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
return { conversations, archivedConversations, pinnedConversations };
|
return { conversations, archivedConversations, pinnedConversations };
|
||||||
|
@ -178,6 +178,7 @@ export const getLeftPaneLists = createSelector(
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
getConversationComparator,
|
getConversationComparator,
|
||||||
getSelectedConversation,
|
getSelectedConversation,
|
||||||
|
getPinnedConversationIds,
|
||||||
_getLeftPaneLists
|
_getLeftPaneLists
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
20
ts/state/selectors/items.ts
Normal file
20
ts/state/selectors/items.ts
Normal file
|
@ -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<string> =>
|
||||||
|
(state.pinnedConversationIds || []) as Array<string>
|
||||||
|
);
|
|
@ -24,7 +24,8 @@ import {
|
||||||
} from '../../components/SearchResults';
|
} from '../../components/SearchResults';
|
||||||
import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/MessageSearchResult';
|
import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/MessageSearchResult';
|
||||||
|
|
||||||
import { getRegionCode, getUserAgent, getUserNumber } from './user';
|
import { getRegionCode, getUserNumber } from './user';
|
||||||
|
import { getUserAgent } from './items';
|
||||||
import {
|
import {
|
||||||
GetConversationByIdType,
|
GetConversationByIdType,
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
|
|
|
@ -7,12 +7,9 @@ import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
import { UserStateType } from '../ducks/user';
|
import { UserStateType } from '../ducks/user';
|
||||||
import { ItemsStateType } from '../ducks/items';
|
|
||||||
|
|
||||||
export const getUser = (state: StateType): UserStateType => state.user;
|
export const getUser = (state: StateType): UserStateType => state.user;
|
||||||
|
|
||||||
export const getItems = (state: StateType): ItemsStateType => state.items;
|
|
||||||
|
|
||||||
export const getUserNumber = createSelector(
|
export const getUserNumber = createSelector(
|
||||||
getUser,
|
getUser,
|
||||||
(state: UserStateType): string => state.ourNumber
|
(state: UserStateType): string => state.ourNumber
|
||||||
|
@ -33,11 +30,6 @@ export const getUserUuid = createSelector(
|
||||||
(state: UserStateType): string => state.ourUuid
|
(state: UserStateType): string => state.ourUuid
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getUserAgent = createSelector(
|
|
||||||
getItems,
|
|
||||||
(state: ItemsStateType): string => state.userAgent as string
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getIntl = createSelector(
|
export const getIntl = createSelector(
|
||||||
getUser,
|
getUser,
|
||||||
(state: UserStateType): LocalizerType => state.i18n
|
(state: UserStateType): LocalizerType => state.i18n
|
||||||
|
|
384
ts/test-both/state/ducks/conversations_test.ts
Normal file
384
ts/test-both/state/ducks/conversations_test.ts
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,7 +2,6 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
|
||||||
|
|
||||||
import { ConversationLookupType } from '../../../state/ducks/conversations';
|
import { ConversationLookupType } from '../../../state/ducks/conversations';
|
||||||
import {
|
import {
|
||||||
|
@ -10,28 +9,7 @@ import {
|
||||||
_getLeftPaneLists,
|
_getLeftPaneLists,
|
||||||
} from '../../../state/selectors/conversations';
|
} from '../../../state/selectors/conversations';
|
||||||
|
|
||||||
describe('state/selectors/conversations', () => {
|
describe('both/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('#getLeftPaneList', () => {
|
describe('#getLeftPaneList', () => {
|
||||||
it('sorts conversations based on timestamp then by intl-friendly title', () => {
|
it('sorts conversations based on timestamp then by intl-friendly title', () => {
|
||||||
const data: ConversationLookupType = {
|
const data: ConversationLookupType = {
|
||||||
|
@ -172,14 +150,6 @@ describe('state/selectors/conversations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('given pinned 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', () => {
|
it('sorts pinned conversations based on order in storage', () => {
|
||||||
const data: ConversationLookupType = {
|
const data: ConversationLookupType = {
|
||||||
pin2: {
|
pin2: {
|
||||||
|
@ -262,8 +232,14 @@ describe('state/selectors/conversations', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pinnedConversationIds = ['pin1', 'pin2', 'pin3'];
|
||||||
const comparator = _getConversationComparator();
|
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[0].name, 'Pin One');
|
||||||
assert.strictEqual(pinnedConversations[1].name, 'Pin Two');
|
assert.strictEqual(pinnedConversations[1].name, 'Pin Two');
|
40
ts/test-both/state/selectors/items_test.ts
Normal file
40
ts/test-both/state/selectors/items_test.ts
Normal file
|
@ -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<string> = [];
|
||||||
|
const state = getDefaultStateType();
|
||||||
|
|
||||||
|
const actual = getPinnedConversationIds(state);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -15007,7 +15007,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-before(",
|
"rule": "jQuery-before(",
|
||||||
"path": "ts/test/util/windowsZoneIdentifier_test.js",
|
"path": "ts/test-node/util/windowsZoneIdentifier_test.js",
|
||||||
"line": " before(function thisNeeded() {",
|
"line": " before(function thisNeeded() {",
|
||||||
"lineNumber": 21,
|
"lineNumber": 21,
|
||||||
"reasonCategory": "testCode",
|
"reasonCategory": "testCode",
|
||||||
|
@ -15016,7 +15016,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-before(",
|
"rule": "jQuery-before(",
|
||||||
"path": "ts/test/util/windowsZoneIdentifier_test.ts",
|
"path": "ts/test-node/util/windowsZoneIdentifier_test.ts",
|
||||||
"line": " before(function thisNeeded() {",
|
"line": " before(function thisNeeded() {",
|
||||||
"lineNumber": 15,
|
"lineNumber": 15,
|
||||||
"reasonCategory": "testCode",
|
"reasonCategory": "testCode",
|
||||||
|
|
|
@ -823,6 +823,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const {
|
const {
|
||||||
messagesAdded,
|
messagesAdded,
|
||||||
setMessagesLoading,
|
setMessagesLoading,
|
||||||
|
repairOldestMessage,
|
||||||
} = window.reduxActions.conversations;
|
} = window.reduxActions.conversations;
|
||||||
const conversationId = this.model.id;
|
const conversationId = this.model.id;
|
||||||
|
|
||||||
|
@ -851,6 +852,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
window.log.warn(
|
window.log.warn(
|
||||||
'loadOlderMessages: requested, but loaded no messages'
|
'loadOlderMessages: requested, but loaded no messages'
|
||||||
);
|
);
|
||||||
|
repairOldestMessage(conversationId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -875,6 +877,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const {
|
const {
|
||||||
messagesAdded,
|
messagesAdded,
|
||||||
setMessagesLoading,
|
setMessagesLoading,
|
||||||
|
repairNewestMessage,
|
||||||
} = window.reduxActions.conversations;
|
} = window.reduxActions.conversations;
|
||||||
const conversationId = this.model.id;
|
const conversationId = this.model.id;
|
||||||
|
|
||||||
|
@ -902,6 +905,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
window.log.warn(
|
window.log.warn(
|
||||||
'loadNewerMessages: requested, but loaded no messages'
|
'loadNewerMessages: requested, but loaded no messages'
|
||||||
);
|
);
|
||||||
|
repairNewestMessage(conversationId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue