mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-24 20:26:24 +00:00
297 lines
9.3 KiB
TypeScript
297 lines
9.3 KiB
TypeScript
// Copyright 2016 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import lodash from 'lodash';
|
|
|
|
import { createLogger } from '../logging/log.std.js';
|
|
|
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary.std.js';
|
|
import { isInPast, isMoreRecentThan } from '../util/timestamp.std.js';
|
|
import { getMessageQueueTime } from '../util/getMessageQueueTime.dom.js';
|
|
import { drop } from '../util/drop.std.js';
|
|
import { DataReader, DataWriter } from '../sql/Client.preload.js';
|
|
import {
|
|
findNextProfileEvent,
|
|
redactNotificationProfileId,
|
|
} from '../types/NotificationProfile.std.js';
|
|
import {
|
|
getActiveProfile,
|
|
getCurrentState,
|
|
getDeletedProfiles,
|
|
getOverride,
|
|
getProfiles,
|
|
} from '../state/selectors/notificationProfiles.dom.js';
|
|
import { safeSetTimeout } from '../util/timeout.std.js';
|
|
import { ToastType } from '../types/Toast.dom.js';
|
|
import { toLogFormat } from '../types/errors.std.js';
|
|
|
|
import type {
|
|
NextProfileEvent,
|
|
NotificationProfileType,
|
|
} from '../types/NotificationProfile.std.js';
|
|
|
|
const { debounce, isEqual, isNumber } = lodash;
|
|
|
|
const log = createLogger('notificationProfilesService');
|
|
|
|
export class NotificationProfilesService {
|
|
#timeout?: ReturnType<typeof setTimeout> | null;
|
|
#debouncedRefreshNextEvent = debounce(this.#refreshNextEvent, 1000);
|
|
|
|
update(): void {
|
|
drop(this.#debouncedRefreshNextEvent());
|
|
}
|
|
|
|
fastUpdate(): void {
|
|
drop(this.#refreshNextEvent());
|
|
}
|
|
|
|
async #refreshNextEvent(): Promise<void> {
|
|
log.info('notificationProfileService: starting');
|
|
|
|
const { updateCurrentState, updateOverride, profileWasRemoved } =
|
|
window.reduxActions.notificationProfiles;
|
|
|
|
const state = window.reduxStore.getState();
|
|
|
|
// This gets everything, even if it's not being shown to user
|
|
const allProfilesIncludingRemoteOnly = state.notificationProfiles.profiles;
|
|
|
|
// These fetches are limited to what user can see (local-only items, if sync=OFF)
|
|
const profiles = getProfiles(state);
|
|
const previousCurrentState = getCurrentState(state);
|
|
const previousActiveProfile = getActiveProfile(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, { fromStorageService: false });
|
|
} 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, { fromStorageService: false });
|
|
} else {
|
|
log.info(
|
|
'notificationProfileService: Tried to clear manual disable override, but it did not match previous override'
|
|
);
|
|
}
|
|
}
|
|
|
|
let currentState: NextProfileEvent;
|
|
try {
|
|
log.info('notificationProfileService: finding next profile event');
|
|
currentState = findNextProfileEvent({
|
|
override,
|
|
profiles,
|
|
time,
|
|
});
|
|
} catch (error) {
|
|
log.warn('notificationProfileService:', toLogFormat(error));
|
|
if (override) {
|
|
log.warn(
|
|
'notificationProfileService: Clearing override because something went wrong'
|
|
);
|
|
|
|
// This will kick off another profile update when it completes
|
|
updateOverride(undefined, { fromStorageService: false });
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const currentActiveProfileId =
|
|
currentState.type === 'willDisable' || currentState.type === 'noChange'
|
|
? currentState.activeProfile
|
|
: undefined;
|
|
const currentActiveProfile = currentActiveProfileId
|
|
? allProfilesIncludingRemoteOnly.find(
|
|
item => item.id === currentActiveProfileId
|
|
)
|
|
: undefined;
|
|
|
|
if (
|
|
!isEqual(previousCurrentState, currentState) ||
|
|
!isEqual(currentActiveProfile, previousActiveProfile)
|
|
) {
|
|
const idForLogging = currentActiveProfile
|
|
? redactNotificationProfileId(currentActiveProfile.id)
|
|
: 'NONE';
|
|
log.info(
|
|
`notificationProfileService: next profile event has changed, updating redux. Active profile is ${idForLogging}`
|
|
);
|
|
updateCurrentState(currentState, currentActiveProfile);
|
|
|
|
// The active profile can influence the overall badge count
|
|
window.Whisper.events.emit('updateUnreadCount');
|
|
}
|
|
|
|
if (previousActiveProfile?.id === currentActiveProfileId) {
|
|
// do nothing!
|
|
// Something has changed, but it's still the same profile
|
|
} else if (
|
|
previousActiveProfile &&
|
|
currentActiveProfile &&
|
|
previousActiveProfile.name === currentActiveProfile.name &&
|
|
// This off-by-one timestamp is created in prepareForDisabledNotificationProfileSync
|
|
(previousActiveProfile.createdAtMs ===
|
|
currentActiveProfile.createdAtMs + 1 ||
|
|
previousActiveProfile.createdAtMs + 1 ===
|
|
currentActiveProfile.createdAtMs)
|
|
) {
|
|
// do nothing!
|
|
// We're switching to a different profile, but it's a remote/local copy. This will
|
|
// happen whenever there's an override enabling a profile and notification profiles
|
|
// sync is turned on/off.
|
|
} else if (!currentActiveProfileId) {
|
|
if (previousActiveProfile) {
|
|
window.reduxActions.toast.showToast({
|
|
toastType: ToastType.NotificationProfileUpdate,
|
|
parameters: {
|
|
enabled: false,
|
|
name: previousActiveProfile.name,
|
|
},
|
|
});
|
|
} else {
|
|
log.warn(
|
|
'refreshNextEvent: Unable to find just-disabled profile for toast'
|
|
);
|
|
}
|
|
} else if (currentActiveProfile) {
|
|
window.reduxActions.toast.showToast({
|
|
toastType: ToastType.NotificationProfileUpdate,
|
|
parameters: {
|
|
enabled: true,
|
|
name: currentActiveProfile.name,
|
|
},
|
|
});
|
|
} else {
|
|
log.warn(
|
|
'refreshNextEvent: Unable to find just-enabled profile for toast'
|
|
);
|
|
}
|
|
|
|
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 = nextCheck - Date.now();
|
|
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('update: NotificationProfileService not yet initialized!');
|
|
}
|
|
instance.update();
|
|
}
|
|
export function fastUpdate(): void {
|
|
if (!instance) {
|
|
throw new Error(
|
|
'fastUpdate: NotificationProfileService not yet initialized!'
|
|
);
|
|
}
|
|
instance.fastUpdate();
|
|
}
|
|
|
|
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;
|