Improve logic for app badge count
This commit is contained in:
parent
95de40662b
commit
59b45399e4
4 changed files with 150 additions and 30 deletions
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { debounce, uniq, without } from 'lodash';
|
import { debounce, uniq, without } from 'lodash';
|
||||||
|
@ -14,8 +14,8 @@ import type { ConversationModel } from './models/conversations';
|
||||||
import { getContactId } from './messages/helpers';
|
import { getContactId } from './messages/helpers';
|
||||||
import { maybeDeriveGroupV2Id } from './groups';
|
import { maybeDeriveGroupV2Id } from './groups';
|
||||||
import { assert } from './util/assert';
|
import { assert } from './util/assert';
|
||||||
import { map, reduce } from './util/iterables';
|
|
||||||
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
|
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
|
||||||
|
import { getConversationUnreadCountForAppBadge } from './util/getConversationUnreadCountForAppBadge';
|
||||||
import { UUID, isValidUuid } from './types/UUID';
|
import { UUID, isValidUuid } from './types/UUID';
|
||||||
import { Address } from './types/Address';
|
import { Address } from './types/Address';
|
||||||
import { QualifiedAddress } from './types/QualifiedAddress';
|
import { QualifiedAddress } from './types/QualifiedAddress';
|
||||||
|
@ -53,7 +53,10 @@ export function start(): void {
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
|
|
||||||
this.on('add remove change:unreadCount', debouncedUpdateUnreadCount);
|
this.on(
|
||||||
|
'add remove change:unreadCount change:markedUnread change:isArchived change:muteExpiresAt',
|
||||||
|
debouncedUpdateUnreadCount
|
||||||
|
);
|
||||||
window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount);
|
window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount);
|
||||||
this.on('add', (model: ConversationModel): void => {
|
this.on('add', (model: ConversationModel): void => {
|
||||||
// If the conversation is muted we set a timeout so when the mute expires
|
// If the conversation is muted we set a timeout so when the mute expires
|
||||||
|
@ -70,32 +73,16 @@ export function start(): void {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateUnreadCount() {
|
updateUnreadCount() {
|
||||||
const canCountMutedConversations = window.storage.get(
|
const canCountMutedConversations =
|
||||||
'badge-count-muted-conversations'
|
window.storage.get('badge-count-muted-conversations') || false;
|
||||||
);
|
|
||||||
|
|
||||||
const canCount = (m: ConversationModel) =>
|
const newUnreadCount = this.reduce(
|
||||||
!m.isMuted() || canCountMutedConversations;
|
(result: number, conversation: ConversationModel) =>
|
||||||
|
result +
|
||||||
const getUnreadCount = (m: ConversationModel) => {
|
getConversationUnreadCountForAppBadge(
|
||||||
const unreadCount = m.get('unreadCount');
|
conversation.attributes,
|
||||||
|
canCountMutedConversations
|
||||||
if (unreadCount) {
|
),
|
||||||
return unreadCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m.get('markedUnread')) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const newUnreadCount = reduce(
|
|
||||||
map(this, (m: ConversationModel) =>
|
|
||||||
canCount(m) ? getUnreadCount(m) : 0
|
|
||||||
),
|
|
||||||
(item: number, memo: number) => (item || 0) + memo,
|
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
window.storage.put('unreadCount', newUnreadCount);
|
window.storage.put('unreadCount', newUnreadCount);
|
||||||
|
|
|
@ -4278,8 +4278,6 @@ export class ConversationModel extends window.Backbone
|
||||||
if (Boolean(previousMarkedUnread) !== Boolean(markedUnread)) {
|
if (Boolean(previousMarkedUnread) !== Boolean(markedUnread)) {
|
||||||
this.captureChange('markedUnread');
|
this.captureChange('markedUnread');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.events.trigger('updateUnreadCount');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshGroupLink(): Promise<void> {
|
async refreshGroupLink(): Promise<void> {
|
||||||
|
|
100
ts/test-both/util/getConversationUnreadCountForAppBadge_test.ts
Normal file
100
ts/test-both/util/getConversationUnreadCountForAppBadge_test.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { getConversationUnreadCountForAppBadge } from '../../util/getConversationUnreadCountForAppBadge';
|
||||||
|
|
||||||
|
describe('getConversationUnreadCountForAppBadge', () => {
|
||||||
|
const getCount = getConversationUnreadCountForAppBadge;
|
||||||
|
|
||||||
|
const mutedTimestamp = (): number => Date.now() + 12345;
|
||||||
|
const oldMutedTimestamp = (): number => Date.now() - 1000;
|
||||||
|
|
||||||
|
it('returns 0 if the conversation is archived', () => {
|
||||||
|
const archivedConversations = [
|
||||||
|
{ isArchived: true, markedUnread: false, unreadCount: 0 },
|
||||||
|
{ isArchived: true, markedUnread: false, unreadCount: 123 },
|
||||||
|
{ isArchived: true, markedUnread: true, unreadCount: 0 },
|
||||||
|
{ isArchived: true, markedUnread: true },
|
||||||
|
];
|
||||||
|
for (const conversation of archivedConversations) {
|
||||||
|
assert.strictEqual(getCount(conversation, true), 0);
|
||||||
|
assert.strictEqual(getCount(conversation, false), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 0 if the conversation is muted and the user doesn't want to include those in the result", () => {
|
||||||
|
const mutedConversations = [
|
||||||
|
{ muteExpiresAt: mutedTimestamp(), markedUnread: false, unreadCount: 0 },
|
||||||
|
{ muteExpiresAt: mutedTimestamp(), markedUnread: false, unreadCount: 9 },
|
||||||
|
{ muteExpiresAt: mutedTimestamp(), markedUnread: true, unreadCount: 0 },
|
||||||
|
{ muteExpiresAt: mutedTimestamp(), markedUnread: true },
|
||||||
|
];
|
||||||
|
for (const conversation of mutedConversations) {
|
||||||
|
assert.strictEqual(getCount(conversation, false), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the unread count if nonzero (and not archived)', () => {
|
||||||
|
const conversationsWithUnreadCount = [
|
||||||
|
{ unreadCount: 9, markedUnread: false },
|
||||||
|
{ unreadCount: 9, markedUnread: true },
|
||||||
|
{
|
||||||
|
unreadCount: 9,
|
||||||
|
markedUnread: false,
|
||||||
|
muteExpiresAt: oldMutedTimestamp(),
|
||||||
|
},
|
||||||
|
{ unreadCount: 9, markedUnread: false, isArchived: false },
|
||||||
|
];
|
||||||
|
for (const conversation of conversationsWithUnreadCount) {
|
||||||
|
assert.strictEqual(getCount(conversation, false), 9);
|
||||||
|
assert.strictEqual(getCount(conversation, true), 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutedWithUnreads = {
|
||||||
|
unreadCount: 123,
|
||||||
|
markedUnread: false,
|
||||||
|
muteExpiresAt: mutedTimestamp(),
|
||||||
|
};
|
||||||
|
assert.strictEqual(getCount(mutedWithUnreads, true), 123);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 1 if the conversation is marked unread', () => {
|
||||||
|
const conversationsMarkedUnread = [
|
||||||
|
{ markedUnread: true },
|
||||||
|
{ markedUnread: true, unreadCount: 0 },
|
||||||
|
{ markedUnread: true, muteExpiresAt: oldMutedTimestamp() },
|
||||||
|
{
|
||||||
|
markedUnread: true,
|
||||||
|
muteExpiresAt: oldMutedTimestamp(),
|
||||||
|
isArchived: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const conversation of conversationsMarkedUnread) {
|
||||||
|
assert.strictEqual(getCount(conversation, false), 1);
|
||||||
|
assert.strictEqual(getCount(conversation, true), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutedConversationsMarkedUnread = [
|
||||||
|
{ markedUnread: true, muteExpiresAt: mutedTimestamp() },
|
||||||
|
{ markedUnread: true, muteExpiresAt: mutedTimestamp(), unreadCount: 0 },
|
||||||
|
];
|
||||||
|
for (const conversation of mutedConversationsMarkedUnread) {
|
||||||
|
assert.strictEqual(getCount(conversation, true), 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 if the conversation is read', () => {
|
||||||
|
const readConversations = [
|
||||||
|
{ markedUnread: false },
|
||||||
|
{ markedUnread: false, unreadCount: 0 },
|
||||||
|
{ markedUnread: false, mutedTimestamp: mutedTimestamp() },
|
||||||
|
{ markedUnread: false, mutedTimestamp: oldMutedTimestamp() },
|
||||||
|
];
|
||||||
|
for (const conversation of readConversations) {
|
||||||
|
assert.strictEqual(getCount(conversation, false), 0);
|
||||||
|
assert.strictEqual(getCount(conversation, true), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
35
ts/util/getConversationUnreadCountForAppBadge.ts
Normal file
35
ts/util/getConversationUnreadCountForAppBadge.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import { isConversationMuted } from './isConversationMuted';
|
||||||
|
|
||||||
|
export function getConversationUnreadCountForAppBadge(
|
||||||
|
conversation: Readonly<
|
||||||
|
Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'isArchived' | 'markedUnread' | 'muteExpiresAt' | 'unreadCount'
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
canCountMutedConversations: boolean
|
||||||
|
): number {
|
||||||
|
const { isArchived, markedUnread, unreadCount } = conversation;
|
||||||
|
|
||||||
|
if (isArchived) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canCountMutedConversations && isConversationMuted(conversation)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unreadCount) {
|
||||||
|
return unreadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markedUnread) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue