Remove inboxCollection, ensure falsey active_at removes from badge count
This commit is contained in:
parent
4a8cdbd687
commit
638e3e3a58
5 changed files with 172 additions and 95 deletions
|
@ -22,6 +22,7 @@ import { QualifiedAddress } from './types/QualifiedAddress';
|
||||||
import * as log from './logging/log';
|
import * as log from './logging/log';
|
||||||
import { sleep } from './util/sleep';
|
import { sleep } from './util/sleep';
|
||||||
import { isNotNil } from './util/isNotNil';
|
import { isNotNil } from './util/isNotNil';
|
||||||
|
import { SECOND } from './util/durations';
|
||||||
|
|
||||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||||
|
|
||||||
|
@ -41,53 +42,54 @@ const {
|
||||||
export function start(): void {
|
export function start(): void {
|
||||||
const conversations = new window.Whisper.ConversationCollection();
|
const conversations = new window.Whisper.ConversationCollection();
|
||||||
|
|
||||||
// This class is entirely designed to keep the app title, badge and tray icon updated.
|
window.getConversations = () => conversations;
|
||||||
// In the future it could listen to redux changes and do its updates there.
|
window.ConversationController = new ConversationController(conversations);
|
||||||
const inboxCollection = new (window.Backbone.Collection.extend({
|
}
|
||||||
hasQueueEmptied: false,
|
|
||||||
|
|
||||||
initialize() {
|
export class ConversationController {
|
||||||
this.listenTo(conversations, 'add change:active_at', this.addActive);
|
private _initialFetchComplete = false;
|
||||||
this.listenTo(conversations, 'reset', () => this.reset([]));
|
|
||||||
|
|
||||||
|
private _initialPromise: undefined | Promise<void>;
|
||||||
|
|
||||||
|
private _conversationOpenStart = new Map<string, number>();
|
||||||
|
|
||||||
|
private _hasQueueEmptied = false;
|
||||||
|
|
||||||
|
constructor(private _conversations: ConversationModelCollectionType) {
|
||||||
const debouncedUpdateUnreadCount = debounce(
|
const debouncedUpdateUnreadCount = debounce(
|
||||||
this.updateUnreadCount.bind(this),
|
this.updateUnreadCount.bind(this),
|
||||||
1000,
|
SECOND,
|
||||||
{ leading: true, maxWait: 1000, trailing: true }
|
{
|
||||||
|
leading: true,
|
||||||
|
maxWait: SECOND,
|
||||||
|
trailing: true,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.on(
|
// A few things can cause us to update the app-level unread count
|
||||||
'add remove change:unreadCount change:markedUnread change:isArchived change:muteExpiresAt',
|
window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount);
|
||||||
|
this._conversations.on(
|
||||||
|
'add remove change:active_at change:unreadCount change:markedUnread change:isArchived change:muteExpiresAt',
|
||||||
debouncedUpdateUnreadCount
|
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
|
// 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
|
// we can reset the mute state on the model. If the mute has already expired
|
||||||
// then we reset the state right away.
|
// then we reset the state right away.
|
||||||
|
this._conversations.on('add', (model: ConversationModel): void => {
|
||||||
model.startMuteTimer();
|
model.startMuteTimer();
|
||||||
});
|
});
|
||||||
},
|
|
||||||
onEmpty() {
|
|
||||||
this.hasQueueEmptied = true;
|
|
||||||
this.updateUnreadCount();
|
|
||||||
},
|
|
||||||
addActive(model: ConversationModel) {
|
|
||||||
if (model.get('active_at')) {
|
|
||||||
this.add(model);
|
|
||||||
} else {
|
|
||||||
this.remove(model);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
updateUnreadCount() {
|
updateUnreadCount(): void {
|
||||||
if (!this.hasQueueEmptied) {
|
if (!this._hasQueueEmptied) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const canCountMutedConversations =
|
const canCountMutedConversations =
|
||||||
window.storage.get('badge-count-muted-conversations') || false;
|
window.storage.get('badge-count-muted-conversations') || false;
|
||||||
|
|
||||||
const newUnreadCount = this.reduce(
|
const newUnreadCount = this._conversations.reduce(
|
||||||
(result: number, conversation: ConversationModel) =>
|
(result: number, conversation: ConversationModel) =>
|
||||||
result +
|
result +
|
||||||
getConversationUnreadCountForAppBadge(
|
getConversationUnreadCountForAppBadge(
|
||||||
|
@ -106,22 +108,12 @@ export function start(): void {
|
||||||
window.document.title = window.getTitle();
|
window.document.title = window.getTitle();
|
||||||
}
|
}
|
||||||
window.updateTrayIcon(newUnreadCount);
|
window.updateTrayIcon(newUnreadCount);
|
||||||
},
|
}
|
||||||
}))();
|
|
||||||
|
|
||||||
window.getInboxCollection = () => inboxCollection;
|
onEmpty(): void {
|
||||||
window.getConversations = () => conversations;
|
this._hasQueueEmptied = true;
|
||||||
window.ConversationController = new ConversationController(conversations);
|
this.updateUnreadCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConversationController {
|
|
||||||
private _initialFetchComplete = false;
|
|
||||||
|
|
||||||
private _initialPromise: undefined | Promise<void>;
|
|
||||||
|
|
||||||
private _conversationOpenStart = new Map<string, number>();
|
|
||||||
|
|
||||||
constructor(private _conversations: ConversationModelCollectionType) {}
|
|
||||||
|
|
||||||
get(id?: string | null): ConversationModel | undefined {
|
get(id?: string | null): ConversationModel | undefined {
|
||||||
if (!this._initialFetchComplete) {
|
if (!this._initialFetchComplete) {
|
||||||
|
|
|
@ -2307,7 +2307,7 @@ export async function startApp(): Promise<void> {
|
||||||
]);
|
]);
|
||||||
log.info('onEmpty: All outstanding database requests complete');
|
log.info('onEmpty: All outstanding database requests complete');
|
||||||
window.readyForUpdates();
|
window.readyForUpdates();
|
||||||
window.getInboxCollection().onEmpty();
|
window.ConversationController.onEmpty();
|
||||||
|
|
||||||
// Start listeners here, after we get through our queue.
|
// Start listeners here, after we get through our queue.
|
||||||
RotateSignedPreKeyListener.init(window.Whisper.events, newVersion);
|
RotateSignedPreKeyListener.init(window.Whisper.events, newVersion);
|
||||||
|
|
|
@ -13,10 +13,25 @@ describe('getConversationUnreadCountForAppBadge', () => {
|
||||||
|
|
||||||
it('returns 0 if the conversation is archived', () => {
|
it('returns 0 if the conversation is archived', () => {
|
||||||
const archivedConversations = [
|
const archivedConversations = [
|
||||||
{ isArchived: true, markedUnread: false, unreadCount: 0 },
|
{
|
||||||
{ isArchived: true, markedUnread: false, unreadCount: 123 },
|
active_at: Date.now(),
|
||||||
{ isArchived: true, markedUnread: true, unreadCount: 0 },
|
isArchived: true,
|
||||||
{ isArchived: true, markedUnread: 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) {
|
for (const conversation of archivedConversations) {
|
||||||
assert.strictEqual(getCount(conversation, true), 0);
|
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", () => {
|
it("returns 0 if the conversation is muted and the user doesn't want to include those in the result", () => {
|
||||||
const mutedConversations = [
|
const mutedConversations = [
|
||||||
{ muteExpiresAt: mutedTimestamp(), markedUnread: false, unreadCount: 0 },
|
{
|
||||||
{ muteExpiresAt: mutedTimestamp(), markedUnread: false, unreadCount: 9 },
|
active_at: Date.now(),
|
||||||
{ muteExpiresAt: mutedTimestamp(), markedUnread: true, unreadCount: 0 },
|
muteExpiresAt: mutedTimestamp(),
|
||||||
{ muteExpiresAt: mutedTimestamp(), markedUnread: true },
|
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) {
|
for (const conversation of mutedConversations) {
|
||||||
assert.strictEqual(getCount(conversation, false), 0);
|
assert.strictEqual(getCount(conversation, false), 0);
|
||||||
|
@ -38,14 +72,20 @@ describe('getConversationUnreadCountForAppBadge', () => {
|
||||||
|
|
||||||
it('returns the unread count if nonzero (and not archived)', () => {
|
it('returns the unread count if nonzero (and not archived)', () => {
|
||||||
const conversationsWithUnreadCount = [
|
const conversationsWithUnreadCount = [
|
||||||
{ unreadCount: 9, markedUnread: false },
|
{ active_at: Date.now(), unreadCount: 9, markedUnread: false },
|
||||||
{ unreadCount: 9, markedUnread: true },
|
{ active_at: Date.now(), unreadCount: 9, markedUnread: true },
|
||||||
{
|
{
|
||||||
|
active_at: Date.now(),
|
||||||
unreadCount: 9,
|
unreadCount: 9,
|
||||||
markedUnread: false,
|
markedUnread: false,
|
||||||
muteExpiresAt: oldMutedTimestamp(),
|
muteExpiresAt: oldMutedTimestamp(),
|
||||||
},
|
},
|
||||||
{ unreadCount: 9, markedUnread: false, isArchived: false },
|
{
|
||||||
|
active_at: Date.now(),
|
||||||
|
unreadCount: 9,
|
||||||
|
markedUnread: false,
|
||||||
|
isArchived: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
for (const conversation of conversationsWithUnreadCount) {
|
for (const conversation of conversationsWithUnreadCount) {
|
||||||
assert.strictEqual(getCount(conversation, false), 9);
|
assert.strictEqual(getCount(conversation, false), 9);
|
||||||
|
@ -53,6 +93,7 @@ describe('getConversationUnreadCountForAppBadge', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutedWithUnreads = {
|
const mutedWithUnreads = {
|
||||||
|
active_at: Date.now(),
|
||||||
unreadCount: 123,
|
unreadCount: 123,
|
||||||
markedUnread: false,
|
markedUnread: false,
|
||||||
muteExpiresAt: mutedTimestamp(),
|
muteExpiresAt: mutedTimestamp(),
|
||||||
|
@ -62,10 +103,15 @@ describe('getConversationUnreadCountForAppBadge', () => {
|
||||||
|
|
||||||
it('returns 1 if the conversation is marked unread', () => {
|
it('returns 1 if the conversation is marked unread', () => {
|
||||||
const conversationsMarkedUnread = [
|
const conversationsMarkedUnread = [
|
||||||
{ markedUnread: true },
|
{ active_at: Date.now(), markedUnread: true },
|
||||||
{ markedUnread: true, unreadCount: 0 },
|
{ active_at: Date.now(), markedUnread: true, unreadCount: 0 },
|
||||||
{ markedUnread: true, muteExpiresAt: oldMutedTimestamp() },
|
|
||||||
{
|
{
|
||||||
|
active_at: Date.now(),
|
||||||
|
markedUnread: true,
|
||||||
|
muteExpiresAt: oldMutedTimestamp(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active_at: Date.now(),
|
||||||
markedUnread: true,
|
markedUnread: true,
|
||||||
muteExpiresAt: oldMutedTimestamp(),
|
muteExpiresAt: oldMutedTimestamp(),
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
@ -77,8 +123,17 @@ describe('getConversationUnreadCountForAppBadge', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutedConversationsMarkedUnread = [
|
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) {
|
for (const conversation of mutedConversationsMarkedUnread) {
|
||||||
assert.strictEqual(getCount(conversation, true), 1);
|
assert.strictEqual(getCount(conversation, true), 1);
|
||||||
|
@ -87,10 +142,35 @@ describe('getConversationUnreadCountForAppBadge', () => {
|
||||||
|
|
||||||
it('returns 0 if the conversation is read', () => {
|
it('returns 0 if the conversation is read', () => {
|
||||||
const readConversations = [
|
const readConversations = [
|
||||||
{ markedUnread: false },
|
{ active_at: Date.now(), markedUnread: false },
|
||||||
{ markedUnread: false, unreadCount: 0 },
|
{ active_at: Date.now(), markedUnread: false, unreadCount: 0 },
|
||||||
{ markedUnread: false, mutedTimestamp: mutedTimestamp() },
|
{
|
||||||
{ markedUnread: false, mutedTimestamp: oldMutedTimestamp() },
|
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) {
|
for (const conversation of readConversations) {
|
||||||
assert.strictEqual(getCount(conversation, false), 0);
|
assert.strictEqual(getCount(conversation, false), 0);
|
||||||
|
|
|
@ -8,13 +8,21 @@ export function getConversationUnreadCountForAppBadge(
|
||||||
conversation: Readonly<
|
conversation: Readonly<
|
||||||
Pick<
|
Pick<
|
||||||
ConversationAttributesType,
|
ConversationAttributesType,
|
||||||
'isArchived' | 'markedUnread' | 'muteExpiresAt' | 'unreadCount'
|
| 'active_at'
|
||||||
|
| 'isArchived'
|
||||||
|
| 'markedUnread'
|
||||||
|
| 'muteExpiresAt'
|
||||||
|
| 'unreadCount'
|
||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
canCountMutedConversations: boolean
|
canCountMutedConversations: boolean
|
||||||
): number {
|
): number {
|
||||||
const { isArchived, markedUnread, unreadCount } = conversation;
|
const { isArchived, markedUnread, unreadCount } = conversation;
|
||||||
|
|
||||||
|
if (!conversation.active_at) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (isArchived) {
|
if (isArchived) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
3
ts/window.d.ts
vendored
3
ts/window.d.ts
vendored
|
@ -195,9 +195,6 @@ declare global {
|
||||||
getEnvironment: typeof getEnvironment;
|
getEnvironment: typeof getEnvironment;
|
||||||
getExpiration: () => string;
|
getExpiration: () => string;
|
||||||
getHostName: () => string;
|
getHostName: () => string;
|
||||||
getInboxCollection: () => ConversationModelCollectionType & {
|
|
||||||
onEmpty: () => void;
|
|
||||||
};
|
|
||||||
getInteractionMode: () => 'mouse' | 'keyboard';
|
getInteractionMode: () => 'mouse' | 'keyboard';
|
||||||
getLocale: () => ElectronLocaleType;
|
getLocale: () => ElectronLocaleType;
|
||||||
getMediaCameraPermissions: () => Promise<boolean>;
|
getMediaCameraPermissions: () => Promise<boolean>;
|
||||||
|
|
Loading…
Reference in a new issue