Remove inboxCollection, ensure falsey active_at removes from badge count

This commit is contained in:
Scott Nonnenberg 2022-05-31 18:26:57 -07:00 committed by GitHub
parent 4a8cdbd687
commit 638e3e3a58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 172 additions and 95 deletions

View File

@ -22,6 +22,7 @@ import { QualifiedAddress } from './types/QualifiedAddress';
import * as log from './logging/log';
import { sleep } from './util/sleep';
import { isNotNil } from './util/isNotNil';
import { SECOND } from './util/durations';
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
@ -41,75 +42,6 @@ const {
export function start(): void {
const conversations = new window.Whisper.ConversationCollection();
// This class is entirely designed to keep the app title, badge and tray icon updated.
// In the future it could listen to redux changes and do its updates there.
const inboxCollection = new (window.Backbone.Collection.extend({
hasQueueEmptied: false,
initialize() {
this.listenTo(conversations, 'add change:active_at', this.addActive);
this.listenTo(conversations, 'reset', () => this.reset([]));
const debouncedUpdateUnreadCount = debounce(
this.updateUnreadCount.bind(this),
1000,
{ leading: true, maxWait: 1000, trailing: true }
);
this.on(
'add remove change:unreadCount change:markedUnread change:isArchived change:muteExpiresAt',
debouncedUpdateUnreadCount
);
window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount);
this.on('add', (model: ConversationModel): void => {
// If the conversation is muted we set a timeout so when the mute expires
// we can reset the mute state on the model. If the mute has already expired
// then we reset the state right away.
model.startMuteTimer();
});
},
onEmpty() {
this.hasQueueEmptied = true;
this.updateUnreadCount();
},
addActive(model: ConversationModel) {
if (model.get('active_at')) {
this.add(model);
} else {
this.remove(model);
}
},
updateUnreadCount() {
if (!this.hasQueueEmptied) {
return;
}
const canCountMutedConversations =
window.storage.get('badge-count-muted-conversations') || false;
const newUnreadCount = this.reduce(
(result: number, conversation: ConversationModel) =>
result +
getConversationUnreadCountForAppBadge(
conversation.attributes,
canCountMutedConversations
),
0
);
window.storage.put('unreadCount', newUnreadCount);
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title = `${window.getTitle()} (${newUnreadCount})`;
} else {
window.setBadgeCount(0);
window.document.title = window.getTitle();
}
window.updateTrayIcon(newUnreadCount);
},
}))();
window.getInboxCollection = () => inboxCollection;
window.getConversations = () => conversations;
window.ConversationController = new ConversationController(conversations);
}
@ -121,7 +53,67 @@ export class ConversationController {
private _conversationOpenStart = new Map<string, number>();
constructor(private _conversations: ConversationModelCollectionType) {}
private _hasQueueEmptied = false;
constructor(private _conversations: ConversationModelCollectionType) {
const debouncedUpdateUnreadCount = debounce(
this.updateUnreadCount.bind(this),
SECOND,
{
leading: true,
maxWait: SECOND,
trailing: true,
}
);
// A few things can cause us to update the app-level unread count
window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount);
this._conversations.on(
'add remove change:active_at change:unreadCount change:markedUnread change:isArchived change:muteExpiresAt',
debouncedUpdateUnreadCount
);
// If the conversation is muted we set a timeout so when the mute expires
// we can reset the mute state on the model. If the mute has already expired
// then we reset the state right away.
this._conversations.on('add', (model: ConversationModel): void => {
model.startMuteTimer();
});
}
updateUnreadCount(): void {
if (!this._hasQueueEmptied) {
return;
}
const canCountMutedConversations =
window.storage.get('badge-count-muted-conversations') || false;
const newUnreadCount = this._conversations.reduce(
(result: number, conversation: ConversationModel) =>
result +
getConversationUnreadCountForAppBadge(
conversation.attributes,
canCountMutedConversations
),
0
);
window.storage.put('unreadCount', newUnreadCount);
if (newUnreadCount > 0) {
window.setBadgeCount(newUnreadCount);
window.document.title = `${window.getTitle()} (${newUnreadCount})`;
} else {
window.setBadgeCount(0);
window.document.title = window.getTitle();
}
window.updateTrayIcon(newUnreadCount);
}
onEmpty(): void {
this._hasQueueEmptied = true;
this.updateUnreadCount();
}
get(id?: string | null): ConversationModel | undefined {
if (!this._initialFetchComplete) {

View File

@ -2307,7 +2307,7 @@ export async function startApp(): Promise<void> {
]);
log.info('onEmpty: All outstanding database requests complete');
window.readyForUpdates();
window.getInboxCollection().onEmpty();
window.ConversationController.onEmpty();
// Start listeners here, after we get through our queue.
RotateSignedPreKeyListener.init(window.Whisper.events, newVersion);

View File

@ -13,10 +13,25 @@ describe('getConversationUnreadCountForAppBadge', () => {
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 },
{
active_at: Date.now(),
isArchived: true,
markedUnread: false,
unreadCount: 0,
},
{
active_at: Date.now(),
isArchived: true,
markedUnread: false,
unreadCount: 123,
},
{
active_at: Date.now(),
isArchived: true,
markedUnread: true,
unreadCount: 0,
},
{ active_at: Date.now(), isArchived: true, markedUnread: true },
];
for (const conversation of archivedConversations) {
assert.strictEqual(getCount(conversation, true), 0);
@ -26,10 +41,29 @@ describe('getConversationUnreadCountForAppBadge', () => {
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 },
{
active_at: Date.now(),
muteExpiresAt: mutedTimestamp(),
markedUnread: false,
unreadCount: 0,
},
{
active_at: Date.now(),
muteExpiresAt: mutedTimestamp(),
markedUnread: false,
unreadCount: 9,
},
{
active_at: Date.now(),
muteExpiresAt: mutedTimestamp(),
markedUnread: true,
unreadCount: 0,
},
{
active_at: Date.now(),
muteExpiresAt: mutedTimestamp(),
markedUnread: true,
},
];
for (const conversation of mutedConversations) {
assert.strictEqual(getCount(conversation, false), 0);
@ -38,14 +72,20 @@ describe('getConversationUnreadCountForAppBadge', () => {
it('returns the unread count if nonzero (and not archived)', () => {
const conversationsWithUnreadCount = [
{ unreadCount: 9, markedUnread: false },
{ unreadCount: 9, markedUnread: true },
{ active_at: Date.now(), unreadCount: 9, markedUnread: false },
{ active_at: Date.now(), unreadCount: 9, markedUnread: true },
{
active_at: Date.now(),
unreadCount: 9,
markedUnread: false,
muteExpiresAt: oldMutedTimestamp(),
},
{ unreadCount: 9, markedUnread: false, isArchived: false },
{
active_at: Date.now(),
unreadCount: 9,
markedUnread: false,
isArchived: false,
},
];
for (const conversation of conversationsWithUnreadCount) {
assert.strictEqual(getCount(conversation, false), 9);
@ -53,6 +93,7 @@ describe('getConversationUnreadCountForAppBadge', () => {
}
const mutedWithUnreads = {
active_at: Date.now(),
unreadCount: 123,
markedUnread: false,
muteExpiresAt: mutedTimestamp(),
@ -62,10 +103,15 @@ describe('getConversationUnreadCountForAppBadge', () => {
it('returns 1 if the conversation is marked unread', () => {
const conversationsMarkedUnread = [
{ markedUnread: true },
{ markedUnread: true, unreadCount: 0 },
{ markedUnread: true, muteExpiresAt: oldMutedTimestamp() },
{ active_at: Date.now(), markedUnread: true },
{ active_at: Date.now(), markedUnread: true, unreadCount: 0 },
{
active_at: Date.now(),
markedUnread: true,
muteExpiresAt: oldMutedTimestamp(),
},
{
active_at: Date.now(),
markedUnread: true,
muteExpiresAt: oldMutedTimestamp(),
isArchived: false,
@ -77,8 +123,17 @@ describe('getConversationUnreadCountForAppBadge', () => {
}
const mutedConversationsMarkedUnread = [
{ markedUnread: true, muteExpiresAt: mutedTimestamp() },
{ markedUnread: true, muteExpiresAt: mutedTimestamp(), unreadCount: 0 },
{
active_at: Date.now(),
markedUnread: true,
muteExpiresAt: mutedTimestamp(),
},
{
active_at: Date.now(),
markedUnread: true,
muteExpiresAt: mutedTimestamp(),
unreadCount: 0,
},
];
for (const conversation of mutedConversationsMarkedUnread) {
assert.strictEqual(getCount(conversation, true), 1);
@ -87,10 +142,35 @@ describe('getConversationUnreadCountForAppBadge', () => {
it('returns 0 if the conversation is read', () => {
const readConversations = [
{ markedUnread: false },
{ markedUnread: false, unreadCount: 0 },
{ markedUnread: false, mutedTimestamp: mutedTimestamp() },
{ markedUnread: false, mutedTimestamp: oldMutedTimestamp() },
{ active_at: Date.now(), markedUnread: false },
{ active_at: Date.now(), markedUnread: false, unreadCount: 0 },
{
active_at: Date.now(),
markedUnread: false,
mutedTimestamp: mutedTimestamp(),
},
{
active_at: Date.now(),
markedUnread: false,
mutedTimestamp: oldMutedTimestamp(),
},
];
for (const conversation of readConversations) {
assert.strictEqual(getCount(conversation, false), 0);
assert.strictEqual(getCount(conversation, true), 0);
}
});
it('returns 0 if the conversation has falsey active_at', () => {
const readConversations = [
{ active_at: undefined, markedUnread: false, unreadCount: 2 },
{ active_at: null, markedUnread: true, unreadCount: 0 },
{
active_at: 0,
unreadCount: 2,
markedUnread: false,
mutedTimestamp: oldMutedTimestamp(),
},
];
for (const conversation of readConversations) {
assert.strictEqual(getCount(conversation, false), 0);

View File

@ -8,13 +8,21 @@ export function getConversationUnreadCountForAppBadge(
conversation: Readonly<
Pick<
ConversationAttributesType,
'isArchived' | 'markedUnread' | 'muteExpiresAt' | 'unreadCount'
| 'active_at'
| 'isArchived'
| 'markedUnread'
| 'muteExpiresAt'
| 'unreadCount'
>
>,
canCountMutedConversations: boolean
): number {
const { isArchived, markedUnread, unreadCount } = conversation;
if (!conversation.active_at) {
return 0;
}
if (isArchived) {
return 0;
}

3
ts/window.d.ts vendored
View File

@ -195,9 +195,6 @@ declare global {
getEnvironment: typeof getEnvironment;
getExpiration: () => string;
getHostName: () => string;
getInboxCollection: () => ConversationModelCollectionType & {
onEmpty: () => void;
};
getInteractionMode: () => 'mouse' | 'keyboard';
getLocale: () => ElectronLocaleType;
getMediaCameraPermissions: () => Promise<boolean>;