Signal-Desktop/ts/services/notificationProfilesService.ts

191 lines
5.7 KiB
TypeScript

// Copyright 2016 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { debounce, isEqual, isNumber } from 'lodash';
import { createLogger } from '../logging/log';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { isInPast, isMoreRecentThan } from '../util/timestamp';
import { getMessageQueueTime } from '../util/getMessageQueueTime';
import { drop } from '../util/drop';
import { DataReader, DataWriter } from '../sql/Client';
import {
findNextProfileEvent,
redactNotificationProfileId,
type NotificationProfileType,
} from '../types/NotificationProfile';
import {
getCurrentState,
getDeletedProfiles,
getOverride,
getProfiles,
} from '../state/selectors/notificationProfiles';
import { safeSetTimeout } from '../util/timeout';
const log = createLogger('notificationProfilesService');
export class NotificationProfilesService {
#timeout?: ReturnType<typeof setTimeout> | null;
#debouncedRefreshNextEvent = debounce(this.#refreshNextEvent, 1000);
update(): void {
drop(this.#debouncedRefreshNextEvent());
}
async #refreshNextEvent() {
log.info('notificationProfileService: starting');
const { updateCurrentState, updateOverride, profileWasRemoved } =
window.reduxActions.notificationProfiles;
const state = window.reduxStore.getState();
const profiles = getProfiles(state);
const previousCurrentState = getCurrentState(state);
const deletedProfiles = getDeletedProfiles(state);
let override = getOverride(state);
if (deletedProfiles.length) {
log.info(
`notificationProfileService: Checking ${deletedProfiles.length} profiles marked as deleted`
);
}
await Promise.all(
deletedProfiles.map(async profile => {
const { id, deletedAtTimestampMs } = profile;
if (!deletedAtTimestampMs) {
log.warn(
`notificationProfileService: Deleted profile ${redactNotificationProfileId(id)} had no deletedAtTimestampMs`
);
return;
}
if (isMoreRecentThan(deletedAtTimestampMs, getMessageQueueTime())) {
return;
}
log.info(
`notificationProfileService: Removing expired profile ${redactNotificationProfileId(id)}, deleted at ${new Date(deletedAtTimestampMs).toISOString()}`
);
await DataWriter.deleteNotificationProfileById(id);
profileWasRemoved(id);
})
);
const time = Date.now();
if (
previousCurrentState.type === 'willDisable' &&
previousCurrentState.clearEnableOverride &&
isInPast(previousCurrentState.willDisableAt - 1)
) {
if (
override?.enabled &&
override.enabled.profileId === previousCurrentState.activeProfile &&
(!override.enabled.endsAtMs ||
override.enabled.endsAtMs === previousCurrentState.willDisableAt)
) {
log.info('notificationProfileService: Clearing manual enable override');
override = undefined;
updateOverride(undefined);
} else {
log.info(
'notificationProfileService: Tried to clear manual enable override, but it did not match previous override'
);
}
} else if (
previousCurrentState.type === 'willEnable' &&
previousCurrentState.clearDisableOverride &&
isInPast(previousCurrentState.willEnableAt - 1)
) {
if (
override?.disabledAtMs &&
override.disabledAtMs < previousCurrentState.willEnableAt
) {
log.info(
'notificationProfileService: Clearing manual disable override'
);
override = undefined;
updateOverride(undefined);
} else {
log.info(
'notificationProfileService: Tried to clear manual disable override, but it did not match previous override'
);
}
}
log.info('notificationProfileService: finding next profile event');
const currentState = findNextProfileEvent({
override,
profiles,
time,
});
if (!isEqual(previousCurrentState, currentState)) {
log.info(
'notificationProfileService: next profile event has changed, updating redux'
);
updateCurrentState(currentState);
}
let nextCheck: number | undefined;
if (currentState.type === 'willDisable') {
nextCheck = currentState.willDisableAt;
} else if (currentState.type === 'willEnable') {
nextCheck = currentState.willEnableAt;
}
clearTimeoutIfNecessary(this.#timeout);
this.#timeout = undefined;
if (!isNumber(nextCheck)) {
log.info(
'notificationProfileService: no future event found. setting no timeout'
);
return;
}
const wait = Date.now() - nextCheck;
log.info(
`notificationProfileService: next check ${new Date(nextCheck).toISOString()};` +
` waiting ${wait}ms`
);
this.#timeout = safeSetTimeout(this.#refreshNextEvent.bind(this), wait, {
clampToMax: true,
});
}
}
export function initialize(): void {
// if (instance) {
// log.warn('NotificationProfileService is already initialized!');
// return;
// }
// instance = new NotificationProfilesService();
}
export function update(): void {
// if (!instance) {
// throw new Error('NotificationProfileService not yet initialized!');
// }
// instance.update();
}
let cachedProfiles: ReadonlyArray<NotificationProfileType> | undefined;
export async function loadCachedProfiles(): Promise<void> {
cachedProfiles = await DataReader.getAllNotificationProfiles();
}
export function getCachedProfiles(): ReadonlyArray<NotificationProfileType> {
const profiles = cachedProfiles;
if (profiles == null) {
throw new Error('getCachedProfiles: Cache is empty!');
}
cachedProfiles = undefined;
return profiles;
}
// let instance: NotificationProfilesService;