465 lines
15 KiB
TypeScript
465 lines
15 KiB
TypeScript
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { ipcRenderer } from 'electron';
|
|
import type { SystemPreferences } from 'electron';
|
|
import { noop } from 'lodash';
|
|
|
|
import type { ZoomFactorType } from '../types/Storage.d';
|
|
import * as Errors from '../types/errors';
|
|
import * as Stickers from '../types/Stickers';
|
|
import * as Settings from '../types/Settings';
|
|
|
|
import { resolveUsernameByLinkBase64 } from '../services/username';
|
|
import { isInCall } from '../state/selectors/calling';
|
|
|
|
import { strictAssert } from './assert';
|
|
import * as Registration from './registration';
|
|
import { lookupConversationWithoutServiceId } from './lookupConversationWithoutServiceId';
|
|
import { createLogger } from '../logging/log';
|
|
import {
|
|
type NotificationClickData,
|
|
notificationService,
|
|
} from '../services/notifications';
|
|
import { StoryViewModeType, StoryViewTargetType } from '../types/Stories';
|
|
import { isValidE164 } from './isValidE164';
|
|
import { fromWebSafeBase64 } from './webSafeBase64';
|
|
import { showConfirmationDialog } from './showConfirmationDialog';
|
|
import type {
|
|
EphemeralSettings,
|
|
SettingsValuesType,
|
|
ThemeType,
|
|
} from './preload';
|
|
import { SystemTraySetting } from '../types/SystemTraySetting';
|
|
import OS from './os/osPreload';
|
|
|
|
const log = createLogger('createIPCEvents');
|
|
|
|
export type IPCEventsValuesType = {
|
|
// IPC-mediated
|
|
autoLaunch: boolean;
|
|
mediaPermissions: boolean;
|
|
mediaCameraPermissions: boolean | undefined;
|
|
zoomFactor: ZoomFactorType;
|
|
};
|
|
|
|
export type IPCEventsCallbacksType = {
|
|
addDarkOverlay: () => void;
|
|
removeDarkOverlay: () => void;
|
|
|
|
cleanupDownloads: () => Promise<void>;
|
|
getIsInCall: () => boolean;
|
|
getMediaAccessStatus: (
|
|
mediaType: 'screen' | 'microphone' | 'camera'
|
|
) => Promise<ReturnType<SystemPreferences['getMediaAccessStatus']>>;
|
|
installStickerPack: (packId: string, key: string) => Promise<void>;
|
|
requestCloseConfirmation: () => Promise<boolean>;
|
|
setMediaPlaybackDisabled: (playbackDisabled: boolean) => void;
|
|
showConversationViaNotification: (data: NotificationClickData) => void;
|
|
showConversationViaToken: (token: string) => void;
|
|
showConversationViaSignalDotMe: (
|
|
kind: string,
|
|
value: string
|
|
) => Promise<void>;
|
|
showKeyboardShortcuts: () => void;
|
|
showGroupViaLink: (value: string) => Promise<void>;
|
|
showReleaseNotes: () => void;
|
|
showStickerPack: (packId: string, key: string) => void;
|
|
shutdown: () => Promise<void>;
|
|
startCallingLobbyViaToken: (token: string) => void;
|
|
unknownSignalLink: () => void;
|
|
uploadStickerPack: (
|
|
manifest: Uint8Array,
|
|
stickers: ReadonlyArray<Uint8Array>
|
|
) => Promise<string>;
|
|
};
|
|
|
|
type ValuesWithGetters = Omit<
|
|
SettingsValuesType,
|
|
// Async - we'll redefine these in IPCEventsGettersType
|
|
| 'autoLaunch'
|
|
| 'localeOverride'
|
|
| 'mediaPermissions'
|
|
| 'mediaCameraPermissions'
|
|
| 'spellCheck'
|
|
| 'contentProtection'
|
|
| 'systemTraySetting'
|
|
| 'themeSetting'
|
|
| 'zoomFactor'
|
|
>;
|
|
|
|
// Right now everything is symmetrical
|
|
type ValuesWithSetters = SettingsValuesType;
|
|
|
|
export type IPCEventsUpdatersType = {
|
|
[Key in keyof EphemeralSettings as IPCEventUpdaterType<Key>]?: (
|
|
value: EphemeralSettings[Key]
|
|
) => void;
|
|
};
|
|
|
|
export type IPCEventGetterType<Key extends keyof SettingsValuesType> =
|
|
`get${Capitalize<Key>}`;
|
|
|
|
export type IPCEventSetterType<Key extends keyof SettingsValuesType> =
|
|
`set${Capitalize<Key>}`;
|
|
|
|
export type IPCEventUpdaterType<Key extends keyof SettingsValuesType> =
|
|
`update${Capitalize<Key>}`;
|
|
|
|
export type ZoomFactorChangeCallback = (zoomFactor: ZoomFactorType) => void;
|
|
export type IPCEventsGettersType = {
|
|
[Key in keyof ValuesWithGetters as IPCEventGetterType<Key>]: () => ValuesWithGetters[Key];
|
|
} & {
|
|
// Async
|
|
getAutoLaunch: () => Promise<boolean>;
|
|
getLocaleOverride: () => Promise<string | null>;
|
|
getMediaPermissions: () => Promise<boolean>;
|
|
getMediaCameraPermissions: () => Promise<boolean>;
|
|
getSpellCheck: () => Promise<boolean>;
|
|
getContentProtection: () => Promise<boolean>;
|
|
getSystemTraySetting: () => Promise<SystemTraySetting>;
|
|
getThemeSetting: () => Promise<ThemeType>;
|
|
getZoomFactor: () => Promise<ZoomFactorType>;
|
|
// Events
|
|
onZoomFactorChange: (callback: ZoomFactorChangeCallback) => void;
|
|
offZoomFactorChange: (callback: ZoomFactorChangeCallback) => void;
|
|
};
|
|
|
|
export type IPCEventsSettersType = {
|
|
[Key in keyof ValuesWithSetters as IPCEventSetterType<Key>]: (
|
|
value: NonNullable<ValuesWithSetters[Key]>
|
|
) => Promise<void>;
|
|
} & {
|
|
setLocaleOverride: (value: string | null) => Promise<void>;
|
|
setMediaPermissions?: (value: boolean) => Promise<void>;
|
|
setMediaCameraPermissions?: (value: boolean) => Promise<void>;
|
|
};
|
|
|
|
export type IPCEventsType = IPCEventsGettersType &
|
|
IPCEventsSettersType &
|
|
IPCEventsUpdatersType &
|
|
IPCEventsCallbacksType;
|
|
|
|
export function createIPCEvents(
|
|
overrideEvents: Partial<IPCEventsType> = {}
|
|
): IPCEventsType {
|
|
let zoomFactorChangeCallbacks: Array<ZoomFactorChangeCallback> = [];
|
|
ipcRenderer.on('zoomFactorChanged', (_event, zoomFactor) => {
|
|
zoomFactorChangeCallbacks.forEach(callback => callback(zoomFactor));
|
|
});
|
|
|
|
return {
|
|
// From IPCEventsValuesType
|
|
getAutoLaunch: async () => {
|
|
return (await window.IPC.getAutoLaunch()) ?? false;
|
|
},
|
|
setAutoLaunch: async (value: boolean) => {
|
|
await window.IPC.setAutoLaunch(value);
|
|
},
|
|
getMediaCameraPermissions: async () => {
|
|
return (await window.IPC.getMediaCameraPermissions()) ?? false;
|
|
},
|
|
setMediaCameraPermissions: async () => {
|
|
const forCamera = true;
|
|
await window.IPC.showPermissionsPopup(false, forCamera);
|
|
},
|
|
getMediaPermissions: async () => {
|
|
return (await window.IPC.getMediaPermissions()) ?? false;
|
|
},
|
|
setMediaPermissions: async () => {
|
|
const forCalling = true;
|
|
await window.IPC.showPermissionsPopup(forCalling, false);
|
|
},
|
|
getZoomFactor: () => {
|
|
return ipcRenderer.invoke('getZoomFactor');
|
|
},
|
|
setZoomFactor: async zoomFactor => {
|
|
ipcRenderer.send('setZoomFactor', zoomFactor);
|
|
},
|
|
|
|
// From IPCEventsGettersType
|
|
onZoomFactorChange: callback => {
|
|
zoomFactorChangeCallbacks.push(callback);
|
|
},
|
|
offZoomFactorChange: toRemove => {
|
|
zoomFactorChangeCallbacks = zoomFactorChangeCallbacks.filter(
|
|
callback => toRemove !== callback
|
|
);
|
|
},
|
|
|
|
// From EphemeralSettings
|
|
getLocaleOverride: async () => {
|
|
return (await getEphemeralSetting('localeOverride')) ?? null;
|
|
},
|
|
setLocaleOverride: async (value: string | null) => {
|
|
await setEphemeralSetting('localeOverride', value);
|
|
window.SignalContext.restartApp();
|
|
},
|
|
getContentProtection: async () => {
|
|
return (
|
|
!window.SignalContext.config.disableScreenSecurity &&
|
|
((await getEphemeralSetting('contentProtection')) ??
|
|
Settings.isContentProtectionEnabledByDefault(
|
|
OS,
|
|
window.SignalContext.config.osRelease
|
|
))
|
|
);
|
|
},
|
|
setContentProtection: async (value: boolean) => {
|
|
await setEphemeralSetting('contentProtection', value);
|
|
},
|
|
getSpellCheck: async () => {
|
|
return (await getEphemeralSetting('spellCheck')) ?? false;
|
|
},
|
|
setSpellCheck: async (value: boolean) => {
|
|
await setEphemeralSetting('spellCheck', value);
|
|
},
|
|
getSystemTraySetting: async () => {
|
|
return (
|
|
(await getEphemeralSetting('systemTraySetting')) ??
|
|
SystemTraySetting.Uninitialized
|
|
);
|
|
},
|
|
setSystemTraySetting: async (value: SystemTraySetting) => {
|
|
await setEphemeralSetting('systemTraySetting', value);
|
|
},
|
|
getThemeSetting: async () => {
|
|
return (await getEphemeralSetting('themeSetting')) ?? 'system';
|
|
},
|
|
setThemeSetting: async (value: ThemeType) => {
|
|
await setEphemeralSetting('themeSetting', value);
|
|
},
|
|
|
|
// From IPCEventsCallbacksType
|
|
addDarkOverlay: () => {
|
|
const elems = document.querySelectorAll('.dark-overlay');
|
|
if (elems.length) {
|
|
return;
|
|
}
|
|
const newOverlay = document.createElement('div');
|
|
newOverlay.className = 'dark-overlay';
|
|
newOverlay.addEventListener('click', () => {
|
|
newOverlay.remove();
|
|
});
|
|
document.body.prepend(newOverlay);
|
|
},
|
|
removeDarkOverlay: () => {
|
|
const elems = document.querySelectorAll('.dark-overlay');
|
|
|
|
for (const elem of elems) {
|
|
elem.remove();
|
|
}
|
|
},
|
|
cleanupDownloads: async () => {
|
|
await ipcRenderer.invoke('cleanup-downloads');
|
|
},
|
|
getIsInCall: (): boolean => {
|
|
return isInCall(window.reduxStore.getState());
|
|
},
|
|
getMediaAccessStatus: async (
|
|
mediaType: 'screen' | 'microphone' | 'camera'
|
|
) => {
|
|
return window.IPC.getMediaAccessStatus(mediaType);
|
|
},
|
|
installStickerPack: async (packId, key) => {
|
|
void Stickers.downloadStickerPack(packId, key, {
|
|
finalStatus: 'installed',
|
|
actionSource: 'ui',
|
|
});
|
|
},
|
|
requestCloseConfirmation: async (): Promise<boolean> => {
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
showConfirmationDialog({
|
|
dialogName: 'closeConfirmation',
|
|
onTopOfEverything: true,
|
|
cancelText: window.i18n(
|
|
'icu:ConfirmationDialog__Title--close-requested-not-now'
|
|
),
|
|
confirmStyle: 'negative',
|
|
title: window.i18n(
|
|
'icu:ConfirmationDialog__Title--in-call-close-requested'
|
|
),
|
|
okText: window.i18n('icu:close'),
|
|
reject: () => reject(),
|
|
resolve: () => resolve(),
|
|
});
|
|
});
|
|
log.info('requestCloseConfirmation: Close confirmed by user.');
|
|
window.reduxActions.calling.hangUpActiveCall(
|
|
'User confirmed in-call close.'
|
|
);
|
|
return true;
|
|
} catch {
|
|
log.info('requestCloseConfirmation: Close cancelled by user.');
|
|
return false;
|
|
}
|
|
},
|
|
setMediaPlaybackDisabled: (playbackDisabled: boolean) => {
|
|
window.reduxActions?.lightbox.setPlaybackDisabled(playbackDisabled);
|
|
if (playbackDisabled) {
|
|
window.reduxActions?.audioPlayer.pauseVoiceNotePlayer();
|
|
}
|
|
},
|
|
showConversationViaNotification({
|
|
conversationId,
|
|
messageId,
|
|
storyId,
|
|
}: NotificationClickData) {
|
|
if (!conversationId) {
|
|
window.reduxActions.app.openInbox();
|
|
} else if (storyId) {
|
|
window.reduxActions.stories.viewStory({
|
|
storyId,
|
|
storyViewMode: StoryViewModeType.Single,
|
|
viewTarget: StoryViewTargetType.Replies,
|
|
});
|
|
} else {
|
|
window.reduxActions.conversations.showConversation({
|
|
conversationId,
|
|
messageId: messageId ?? undefined,
|
|
});
|
|
}
|
|
},
|
|
showConversationViaToken(token: string) {
|
|
const data = notificationService.resolveToken(token);
|
|
if (!data) {
|
|
window.reduxActions.app.openInbox();
|
|
} else {
|
|
window.Events.showConversationViaNotification(data);
|
|
}
|
|
},
|
|
async showConversationViaSignalDotMe(kind: string, value: string) {
|
|
if (!Registration.everDone()) {
|
|
log.info(
|
|
'showConversationViaSignalDotMe: Not registered, returning early'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const { showUserNotFoundModal } = window.reduxActions.globalModals;
|
|
|
|
let conversationId: string | undefined;
|
|
|
|
try {
|
|
if (kind === 'phoneNumber') {
|
|
if (isValidE164(value, true)) {
|
|
conversationId = await lookupConversationWithoutServiceId({
|
|
type: 'e164',
|
|
e164: value,
|
|
phoneNumber: value,
|
|
showUserNotFoundModal,
|
|
setIsFetchingUUID: noop,
|
|
});
|
|
}
|
|
} else if (kind === 'encryptedUsername') {
|
|
const usernameBase64 = fromWebSafeBase64(value);
|
|
const username = await resolveUsernameByLinkBase64(usernameBase64);
|
|
if (username != null) {
|
|
conversationId = await lookupConversationWithoutServiceId({
|
|
type: 'username',
|
|
username,
|
|
showUserNotFoundModal,
|
|
setIsFetchingUUID: noop,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (conversationId != null) {
|
|
window.reduxActions.conversations.showConversation({
|
|
conversationId,
|
|
});
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
log.warn(
|
|
'showConversationViaSignalDotMe: got error',
|
|
Errors.toLogFormat(error)
|
|
);
|
|
showUnknownSgnlLinkModal();
|
|
return;
|
|
}
|
|
|
|
log.info('showConversationViaSignalDotMe: invalid E164');
|
|
showUnknownSgnlLinkModal();
|
|
},
|
|
showKeyboardShortcuts: () =>
|
|
window.reduxActions.globalModals.showShortcutGuideModal(),
|
|
showGroupViaLink: async value => {
|
|
// We can get these events even if the user has never linked this instance.
|
|
if (!Registration.everDone()) {
|
|
log.warn('showGroupViaLink: Not registered, returning early');
|
|
return;
|
|
}
|
|
try {
|
|
await window.Signal.Groups.joinViaLink(value);
|
|
} catch (error) {
|
|
log.error(
|
|
'showGroupViaLink: Ran into an error!',
|
|
Errors.toLogFormat(error)
|
|
);
|
|
window.reduxActions.globalModals.showErrorModal({
|
|
title: window.i18n('icu:GroupV2--join--general-join-failure--title'),
|
|
description: window.i18n('icu:GroupV2--join--general-join-failure'),
|
|
});
|
|
}
|
|
},
|
|
showReleaseNotes: () => {
|
|
const { showWhatsNewModal } = window.reduxActions.globalModals;
|
|
showWhatsNewModal();
|
|
},
|
|
showStickerPack: (packId, key) => {
|
|
// We can get these events even if the user has never linked this instance.
|
|
if (!Registration.everDone()) {
|
|
log.warn('showStickerPack: Not registered, returning early');
|
|
return;
|
|
}
|
|
window.reduxActions.globalModals.showStickerPackPreview(packId, key);
|
|
},
|
|
shutdown: () => Promise.resolve(),
|
|
startCallingLobbyViaToken(token: string) {
|
|
const data = notificationService.resolveToken(token);
|
|
if (!data) {
|
|
return;
|
|
}
|
|
window.reduxActions?.calling?.startCallingLobby({
|
|
conversationId: data.conversationId,
|
|
isVideoCall: true,
|
|
});
|
|
},
|
|
unknownSignalLink: () => {
|
|
log.warn('unknownSignalLink: Showing error dialog');
|
|
showUnknownSgnlLinkModal();
|
|
},
|
|
uploadStickerPack: (
|
|
manifest: Uint8Array,
|
|
stickers: ReadonlyArray<Uint8Array>
|
|
): Promise<string> => {
|
|
strictAssert(window.textsecure.server, 'WebAPI must be available');
|
|
return window.textsecure.server.putStickers(manifest, stickers, () =>
|
|
ipcRenderer.send('art-creator:onUploadProgress')
|
|
);
|
|
},
|
|
...overrideEvents,
|
|
};
|
|
}
|
|
|
|
function showUnknownSgnlLinkModal(): void {
|
|
window.reduxActions.globalModals.showErrorModal({
|
|
description: window.i18n('icu:unknown-sgnl-link'),
|
|
});
|
|
}
|
|
|
|
function getEphemeralSetting<Name extends keyof EphemeralSettings>(
|
|
name: Name
|
|
): Promise<EphemeralSettings[Name]> {
|
|
return ipcRenderer.invoke(`settings:get:${name}`);
|
|
}
|
|
|
|
function setEphemeralSetting<Name extends keyof EphemeralSettings>(
|
|
name: Name,
|
|
value: EphemeralSettings[Name]
|
|
): Promise<void> {
|
|
return ipcRenderer.invoke(`settings:set:${name}`, value);
|
|
}
|