Fix a few pinned chat sync issues

This commit is contained in:
Chris Svenningsen 2020-10-10 07:25:17 -07:00 committed by Josh Perez
parent e8664213d3
commit fdc3476106
8 changed files with 205 additions and 20 deletions

View file

@ -732,4 +732,37 @@ export class ConversationController {
return this._initialPromise;
}
getPinnedConversationIds(): Array<string> {
let pinnedConversationIds = window.storage.get<Array<string>>(
'pinnedConversationIds'
);
// If pinnedConversationIds is missing, we're upgrading from
// a previous version and need to backfill storage from pinned
// conversation models.
if (pinnedConversationIds === undefined) {
window.log.info(
'getPinnedConversationIds: no pinned conversations in storage'
);
const modelPinnedConversationIds = this._conversations
.filter(conversation => conversation.get('isPinned'))
// pinIndex is a deprecated field. We now rely on the order of
// the ids in storage, which is synced with the AccountRecord.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.sort((a, b) => (a.get('pinIndex') || 0) - (b.get('pinIndex') || 0))
.map(conversation => conversation.get('id'));
window.log.info(
`getPinnedConversationIds: falling back to ${modelPinnedConversationIds.length} pinned models`
);
window.storage.put('pinnedConversationIds', modelPinnedConversationIds);
pinnedConversationIds = modelPinnedConversationIds;
}
return pinnedConversationIds;
}
}

1
ts/model-types.d.ts vendored
View file

@ -153,7 +153,6 @@ export type ConversationAttributesType = {
messageCountBeforeMessageRequests: number;
messageRequestResponseType: number;
muteExpiresAt: number;
pinIndex?: number;
profileAvatar: WhatIsThis;
profileKeyCredential: string | null;
profileKeyVersion: string | null;

View file

@ -1098,7 +1098,6 @@ export class ConversationModel extends window.Backbone.Model<
muteExpiresAt: this.get('muteExpiresAt')!,
name: this.get('name')!,
phoneNumber: this.getNumber()!,
pinIndex: this.get('pinIndex'),
profileName: this.getProfileName()!,
sharedGroupNames: this.get('sharedGroupNames')!,
shouldShowDraft,
@ -4073,17 +4072,16 @@ export class ConversationModel extends window.Backbone.Model<
window.storage.get<Array<string>>('pinnedConversationIds', [])
);
pinnedConversationIds.add(this.id);
this.writePinnedConversations([...pinnedConversationIds]);
this.set('isPinned', true);
this.set('pinIndex', pinnedConversationIds.size);
window.Signal.Data.updateConversation(this.attributes);
if (this.get('isArchived')) {
this.setArchived(false);
}
pinnedConversationIds.add(this.id);
this.writePinnedConversations([...pinnedConversationIds]);
}
unpin(): void {
@ -4093,13 +4091,12 @@ export class ConversationModel extends window.Backbone.Model<
window.storage.get<Array<string>>('pinnedConversationIds', [])
);
this.set('isPinned', false);
this.set('pinIndex', undefined);
window.Signal.Data.updateConversation(this.attributes);
pinnedConversationIds.delete(this.id);
this.writePinnedConversations([...pinnedConversationIds]);
this.set('isPinned', false);
window.Signal.Data.updateConversation(this.attributes);
}
writePinnedConversations(pinnedConversationIds: Array<string>): void {

View file

@ -147,7 +147,7 @@ export async function toAccountRecord(
window.storage.get('typingIndicators')
);
accountRecord.linkPreviews = Boolean(window.storage.get('linkPreviews'));
accountRecord.pinnedConversations = window.storage
const pinnedConversations = window.storage
.get<Array<string>>('pinnedConversationIds', [])
.map(id => {
const pinnedConversation = window.ConversationController.get(id);
@ -197,6 +197,11 @@ export async function toAccountRecord(
pinnedConversationClass !== undefined
);
window.log.info(
`toAccountRecord: sending ${pinnedConversations.length} pinned conversations`
);
accountRecord.pinnedConversations = pinnedConversations;
applyUnknownFields(accountRecord, conversation);
return accountRecord;
@ -580,8 +585,33 @@ export async function mergeAccountRecord(
}
if (remotelyPinnedConversationClasses) {
const locallyPinnedConversations = window.ConversationController._conversations.filter(
conversation => Boolean(conversation.get('isPinned'))
const modelPinnedConversations = window
.getConversations()
.filter(conversation => Boolean(conversation.get('isPinned')));
const modelPinnedConversationIds = modelPinnedConversations.map(
conversation => conversation.get('id')
);
const missingStoragePinnedConversationIds = window.ConversationController.getPinnedConversationIds().filter(
id => !modelPinnedConversationIds.includes(id)
);
if (missingStoragePinnedConversationIds.length !== 0) {
window.log.info(
'mergeAccountRecord: pinnedConversationIds in storage does not match pinned Conversation models'
);
}
const locallyPinnedConversations = modelPinnedConversations.concat(
missingStoragePinnedConversationIds
.map(conversationId =>
window.ConversationController.get(conversationId)
)
.filter(
(conversation): conversation is ConversationModel =>
conversation !== undefined
)
);
window.log.info(
@ -677,12 +707,12 @@ export async function mergeAccountRecord(
);
conversationsToUnpin.forEach(conversation => {
conversation.set({ isPinned: false, pinIndex: undefined });
conversation.set({ isPinned: false });
updateConversation(conversation.attributes);
});
remotelyPinnedConversations.forEach((conversation, index) => {
conversation.set({ isPinned: true, pinIndex: index });
remotelyPinnedConversations.forEach(conversation => {
conversation.set({ isPinned: true });
updateConversation(conversation.attributes);
});

View file

@ -55,7 +55,6 @@ export type ConversationType = {
text: string;
};
phoneNumber?: string;
pinIndex?: number;
membersCount?: number;
muteExpiresAt?: number;
type: ConversationTypeType;

View file

@ -158,7 +158,12 @@ export const _getLeftPaneLists = (
conversations.sort(comparator);
archivedConversations.sort(comparator);
pinnedConversations.sort((a, b) => (a.pinIndex || 0) - (b.pinIndex || 0));
const pinnedConversationIds = window.ConversationController.getPinnedConversationIds();
pinnedConversations.sort(
(a, b) =>
pinnedConversationIds.indexOf(a.id) - pinnedConversationIds.indexOf(b.id)
);
return { conversations, archivedConversations, pinnedConversations };
};

View file

@ -1,4 +1,5 @@
import { assert } from 'chai';
import * as sinon from 'sinon';
import { ConversationLookupType } from '../../../state/ducks/conversations';
import {
@ -7,6 +8,27 @@ import {
} 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.ConversationController = {
getPinnedConversationIds: 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', () => {
it('sorts conversations based on timestamp then by intl-friendly title', () => {
const data: ConversationLookupType = {
@ -140,5 +162,103 @@ describe('state/selectors/conversations', () => {
assert.strictEqual(conversations[3].name, 'C');
assert.strictEqual(conversations[4].name, 'No timestamp');
});
describe('given pinned conversations', () => {
beforeEach(() => {
(window.ConversationController
.getPinnedConversationIds as sinon.SinonStub).returns([
'pin1',
'pin2',
'pin3',
]);
});
it('sorts pinned conversations based on order in storage', () => {
const data: ConversationLookupType = {
pin2: {
id: 'pin2',
e164: '+18005551111',
activeAt: Date.now(),
name: 'Pin Two',
timestamp: 30,
inboxPosition: 30,
phoneNumber: 'notused',
isArchived: false,
isPinned: true,
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'Pin Two',
unreadCount: 1,
isSelected: false,
typingContact: {
name: 'Someone There',
color: 'blue',
phoneNumber: '+18005551111',
},
acceptedMessageRequest: true,
},
pin3: {
id: 'pin3',
e164: '+18005551111',
activeAt: Date.now(),
name: 'Pin Three',
timestamp: 30,
inboxPosition: 30,
phoneNumber: 'notused',
isArchived: false,
isPinned: true,
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'Pin Three',
unreadCount: 1,
isSelected: false,
typingContact: {
name: 'Someone There',
color: 'blue',
phoneNumber: '+18005551111',
},
acceptedMessageRequest: true,
},
pin1: {
id: 'pin1',
e164: '+18005551111',
activeAt: Date.now(),
name: 'Pin One',
timestamp: 30,
inboxPosition: 30,
phoneNumber: 'notused',
isArchived: false,
isPinned: true,
type: 'direct',
isMe: false,
lastUpdated: Date.now(),
title: 'Pin One',
unreadCount: 1,
isSelected: false,
typingContact: {
name: 'Someone There',
color: 'blue',
phoneNumber: '+18005551111',
},
acceptedMessageRequest: true,
},
};
const comparator = _getConversationComparator();
const { pinnedConversations } = _getLeftPaneLists(data, comparator);
assert.strictEqual(pinnedConversations[0].name, 'Pin One');
assert.strictEqual(pinnedConversations[1].name, 'Pin Two');
assert.strictEqual(pinnedConversations[2].name, 'Pin Three');
});
});
});
});

View file

@ -416,7 +416,9 @@ Whisper.ConversationView = Whisper.View.extend({
setPin(value: boolean) {
if (value) {
if (window.storage.get('pinnedConversationIds', []).length >= 4) {
const pinnedConversationIds = window.ConversationController.getPinnedConversationIds();
if (pinnedConversationIds.length >= 4) {
this.showToast(Whisper.PinnedConversationsFullToast);
return;
}