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

View File

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

View File

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

View File

@ -3553,8 +3553,14 @@ export class ConversationModel extends window.Backbone.Model<
}
isMuted(): boolean {
return (this.get('muteExpiresAt') &&
Date.now() < this.get('muteExpiresAt')) as boolean;
return (
Boolean(this.get('muteExpiresAt')) &&
Date.now() < this.get('muteExpiresAt')
);
}
getMuteTimeoutId(): string {
return `mute(${this.get('id')})`;
}
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() {
const muteExpiresAt = this.model.get('muteExpiresAt');
if (!muteExpiresAt) {
if (!this.model.isMuted()) {
return;
}
@ -2614,10 +2614,29 @@ Whisper.ConversationView = Whisper.View.extend({
}
},
setMuteNotifications(ms: any) {
this.model.set({
muteExpiresAt: ms > 0 ? Date.now() + ms : undefined,
});
setMuteNotifications(ms: number) {
const 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() {

2
ts/window.d.ts vendored
View File

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