Cleans up mute state after mute expires

This commit is contained in:
Josh Perez 2020-09-30 20:43:24 -04:00 committed by Josh Perez
parent a581f6ea81
commit 9510fd1eec
7 changed files with 125 additions and 8 deletions

View file

@ -121,6 +121,7 @@ const {
} = require('../../ts/services/updateListener'); } = require('../../ts/services/updateListener');
const { notify } = require('../../ts/services/notify'); const { notify } = require('../../ts/services/notify');
const { calling } = require('../../ts/services/calling'); const { calling } = require('../../ts/services/calling');
const { onTimeout, removeTimeout } = require('../../ts/services/timers');
const { const {
enableStorageService, enableStorageService,
eraseAllStorageServiceState, eraseAllStorageServiceState,
@ -341,7 +342,9 @@ exports.setup = (options = {}) => {
initializeGroupCredentialFetcher, initializeGroupCredentialFetcher,
initializeNetworkObserver, initializeNetworkObserver,
initializeUpdateListener, initializeUpdateListener,
onTimeout,
notify, notify,
removeTimeout,
runStorageServiceSyncJob, runStorageServiceSyncJob,
storageServiceUploadJob, storageServiceUploadJob,
}; };

View file

@ -40,6 +40,9 @@ export function start(): void {
this.on('add remove change:unreadCount', debouncedUpdateUnreadCount); this.on('add remove change:unreadCount', debouncedUpdateUnreadCount);
window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount); window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount);
this.on('add', (model: ConversationModel): void => {
this.initMuteExpirationTimer(model);
});
}, },
addActive(model: ConversationModel) { addActive(model: ConversationModel) {
if (model.get('active_at')) { if (model.get('active_at')) {
@ -48,6 +51,22 @@ export function start(): void {
this.remove(model); this.remove(model);
} }
}, },
// 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.
initMuteExpirationTimer(model: ConversationModel): void {
if (model.isMuted()) {
window.Signal.Services.onTimeout(
model.get('muteExpiresAt'),
() => {
model.set({ muteExpiresAt: undefined });
},
model.getMuteTimeoutId()
);
} else if (model.get('muteExpiresAt')) {
model.set({ muteExpiresAt: undefined });
}
},
updateUnreadCount() { updateUnreadCount() {
const canCountMutedConversations = window.storage.get( const canCountMutedConversations = window.storage.get(
'badge-count-muted-conversations' 'badge-count-muted-conversations'

View file

@ -206,7 +206,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
: null : null
)} )}
> >
{muteExpiresAt && ( {muteExpiresAt && Date.now() < muteExpiresAt && (
<span className="module-conversation-list-item__muted" /> <span className="module-conversation-list-item__muted" />
)} )}
{!isAccepted ? ( {!isAccepted ? (

View file

@ -3553,8 +3553,14 @@ export class ConversationModel extends window.Backbone.Model<
} }
isMuted(): boolean { isMuted(): boolean {
return (this.get('muteExpiresAt') && return (
Date.now() < this.get('muteExpiresAt')) as boolean; Boolean(this.get('muteExpiresAt')) &&
Date.now() < this.get('muteExpiresAt')
);
}
getMuteTimeoutId(): string {
return `mute(${this.get('id')})`;
} }
async notify(message: WhatIsThis, reaction?: WhatIsThis): Promise<void> { async notify(message: WhatIsThis, reaction?: WhatIsThis): Promise<void> {

68
ts/services/timers.ts Normal file
View file

@ -0,0 +1,68 @@
import { v4 as getGuid } from 'uuid';
type TimeoutType = {
timestamp: number;
uuid: string;
};
const timeoutStore: Map<string, () => void> = new Map();
const allTimeouts: Set<TimeoutType> = new Set();
setInterval(() => {
if (!allTimeouts.size) {
return;
}
const now = Date.now();
allTimeouts.forEach((timeout: TimeoutType) => {
const { timestamp, uuid } = timeout;
if (now >= timestamp) {
if (timeoutStore.has(uuid)) {
const callback = timeoutStore.get(uuid);
if (callback) {
callback();
}
timeoutStore.delete(uuid);
}
allTimeouts.delete(timeout);
}
});
}, 100);
export function onTimeout(
timestamp: number,
callback: () => void,
id?: string
): string {
if (id && timeoutStore.has(id)) {
throw new ReferenceError(`onTimeout: ${id} already exists`);
}
let uuid = id || getGuid();
while (timeoutStore.has(uuid)) {
uuid = getGuid();
}
timeoutStore.set(uuid, callback);
allTimeouts.add({
timestamp,
uuid,
});
return uuid;
}
export function removeTimeout(uuid: string): void {
if (timeoutStore.has(uuid)) {
timeoutStore.delete(uuid);
}
allTimeouts.forEach((timeout: TimeoutType) => {
if (uuid === timeout.uuid) {
allTimeouts.delete(timeout);
}
});
}

View file

@ -395,7 +395,7 @@ Whisper.ConversationView = Whisper.View.extend({
getMuteExpirationLabel() { getMuteExpirationLabel() {
const muteExpiresAt = this.model.get('muteExpiresAt'); const muteExpiresAt = this.model.get('muteExpiresAt');
if (!muteExpiresAt) { if (!this.model.isMuted()) {
return; return;
} }
@ -2614,10 +2614,29 @@ Whisper.ConversationView = Whisper.View.extend({
} }
}, },
setMuteNotifications(ms: any) { setMuteNotifications(ms: number) {
this.model.set({ const muteExpiresAt = ms > 0 ? Date.now() + ms : undefined;
muteExpiresAt: ms > 0 ? Date.now() + ms : undefined,
}); if (muteExpiresAt) {
// we use a timeoutId here so that we can reference the mute that was
// potentially set in the ConversationController. Specifically for a
// scenario where a conversation is already muted and we boot up the app,
// a timeout will be already set. But if we change the mute to a later
// date a new timeout would need to be set and the old one cleared. With
// this ID we can reference the existing timeout.
const timeoutId = this.model.getMuteTimeoutId();
window.Signal.Services.removeTimeout(timeoutId);
window.Signal.Services.onTimeout(
muteExpiresAt,
() => {
this.setMuteNotifications(0);
},
timeoutId
);
}
this.model.set({ muteExpiresAt });
this.saveModel();
}, },
async destroyMessages() { async destroyMessages() {

2
ts/window.d.ts vendored
View file

@ -190,6 +190,8 @@ declare global {
updates: WhatIsThis, updates: WhatIsThis,
events: WhatIsThis events: WhatIsThis
) => void; ) => void;
onTimeout: (timestamp: number, cb: () => void, id?: string) => string;
removeTimeout: (uuid: string) => void;
runStorageServiceSyncJob: () => Promise<void>; runStorageServiceSyncJob: () => Promise<void>;
storageServiceUploadJob: () => void; storageServiceUploadJob: () => void;
}; };