signal-desktop/ts/util/createIPCEvents.ts

527 lines
18 KiB
TypeScript
Raw Normal View History

2021-08-18 20:08:14 +00:00
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { webFrame } from 'electron';
import { AudioDevice } from '../types/Calling';
import { ZoomFactorType } from '../types/Storage.d';
import {
DEFAULT_CONVERSATION_COLOR,
ConversationColorType,
CustomColorType,
DefaultConversationColorType,
} from '../types/Colors';
import * as Stickers from '../types/Stickers';
import {
SystemTraySetting,
parseSystemTraySetting,
} from '../types/SystemTraySetting';
import { ConversationType } from '../state/ducks/conversations';
import { calling } from '../services/calling';
import { getConversationsWithCustomColorSelector } from '../state/selectors/conversations';
import { getCustomColors } from '../state/selectors/items';
2021-08-28 13:27:38 +00:00
import { trigger } from '../shims/events';
2021-08-18 20:08:14 +00:00
import { themeChanged } from '../shims/themeChanged';
2021-08-24 20:57:34 +00:00
import { renderClearingDataView } from '../shims/renderClearingDataView';
2021-08-18 20:08:14 +00:00
import * as universalExpireTimer from './universalExpireTimer';
import { PhoneNumberDiscoverability } from './phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from './phoneNumberSharingMode';
import { assert } from './assert';
import * as durations from './durations';
2021-08-19 01:04:38 +00:00
import { isPhoneNumberSharingEnabled } from './isPhoneNumberSharingEnabled';
2021-08-28 13:27:38 +00:00
import { parseE164FromSignalDotMeHash } from './sgnlHref';
2021-08-18 20:08:14 +00:00
type ThemeType = 'light' | 'dark' | 'system';
type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
export type IPCEventsValuesType = {
alwaysRelayCalls: boolean | undefined;
audioNotification: boolean | undefined;
autoDownloadUpdate: boolean;
2021-08-18 20:08:14 +00:00
autoLaunch: boolean;
callRingtoneNotification: boolean;
callSystemNotification: boolean;
countMutedConversations: boolean;
hideMenuBar: boolean | undefined;
incomingCallNotification: boolean;
lastSyncTime: number | undefined;
notificationDrawAttention: boolean;
notificationSetting: NotificationSettingType;
preferredAudioInputDevice: AudioDevice | undefined;
preferredAudioOutputDevice: AudioDevice | undefined;
preferredVideoInputDevice: string | undefined;
spellCheck: boolean;
systemTraySetting: SystemTraySetting;
themeSetting: ThemeType;
universalExpireTimer: number;
zoomFactor: ZoomFactorType;
// Optional
mediaPermissions: boolean;
mediaCameraPermissions: boolean;
// Only getters
blockedCount: number;
linkPreviewSetting: boolean;
phoneNumberDiscoverabilitySetting: PhoneNumberDiscoverability;
phoneNumberSharingSetting: PhoneNumberSharingMode;
readReceiptSetting: boolean;
typingIndicatorSetting: boolean;
deviceName: string | undefined;
};
export type IPCEventsCallbacksType = {
getAvailableIODevices(): Promise<{
availableCameras: Array<
Pick<MediaDeviceInfo, 'deviceId' | 'groupId' | 'kind' | 'label'>
>;
availableMicrophones: Array<AudioDevice>;
availableSpeakers: Array<AudioDevice>;
}>;
addCustomColor: (customColor: CustomColorType) => void;
addDarkOverlay: () => void;
deleteAllData: () => Promise<void>;
editCustomColor: (colorId: string, customColor: CustomColorType) => void;
getConversationsWithCustomColor: (x: string) => Array<ConversationType>;
installStickerPack: (packId: string, key: string) => Promise<void>;
2021-08-19 01:04:38 +00:00
isPhoneNumberSharingEnabled: () => boolean;
2021-08-18 20:08:14 +00:00
isPrimary: () => boolean;
removeCustomColor: (x: string) => void;
removeCustomColorOnConversations: (x: string) => void;
removeDarkOverlay: () => void;
resetAllChatColors: () => void;
resetDefaultChatColor: () => void;
2021-08-28 13:27:38 +00:00
showConversationViaSignalDotMe: (hash: string) => void;
2021-08-18 20:08:14 +00:00
showKeyboardShortcuts: () => void;
showGroupViaLink: (x: string) => Promise<void>;
showStickerPack: (packId: string, key: string) => void;
shutdown: () => Promise<void>;
unknownSignalLink: () => void;
getCustomColors: () => Record<string, CustomColorType>;
syncRequest: () => Promise<void>;
setGlobalDefaultConversationColor: (
color: ConversationColorType,
customColor?: { id: string; value: CustomColorType }
) => void;
getDefaultConversationColor: () => DefaultConversationColorType;
};
type ValuesWithGetters = Omit<
IPCEventsValuesType,
// Optional
'mediaPermissions' | 'mediaCameraPermissions'
>;
type ValuesWithSetters = Omit<
IPCEventsValuesType,
| 'blockedCount'
| 'defaultConversationColor'
| 'linkPreviewSetting'
| 'phoneNumberDiscoverabilitySetting'
| 'phoneNumberSharingSetting'
| 'readReceiptSetting'
| 'typingIndicatorSetting'
| 'deviceName'
// Optional
| 'mediaPermissions'
| 'mediaCameraPermissions'
>;
export type IPCEventGetterType<
Key extends keyof IPCEventsValuesType
> = `get${Capitalize<Key>}`;
export type IPCEventSetterType<
Key extends keyof IPCEventsValuesType
> = `set${Capitalize<Key>}`;
export type IPCEventsGettersType = {
[Key in keyof ValuesWithGetters as IPCEventGetterType<Key>]: () => ValuesWithGetters[Key];
} & {
getMediaPermissions?: () => Promise<boolean>;
getMediaCameraPermissions?: () => Promise<boolean>;
};
export type IPCEventsSettersType = {
[Key in keyof ValuesWithSetters as IPCEventSetterType<Key>]: (
value: NonNullable<ValuesWithSetters[Key]>
) => Promise<void>;
} & {
setMediaPermissions?: (value: boolean) => Promise<void>;
setMediaCameraPermissions?: (value: boolean) => Promise<void>;
};
export type IPCEventsType = IPCEventsGettersType &
IPCEventsSettersType &
IPCEventsCallbacksType;
export function createIPCEvents(
overrideEvents: Partial<IPCEventsType> = {}
): IPCEventsType {
return {
getDeviceName: () => window.textsecure.storage.user.getDeviceName(),
getZoomFactor: () => window.storage.get('zoomFactor', 1),
setZoomFactor: (zoomFactor: ZoomFactorType) => {
const numZoomFactor = zoomFactor;
webFrame.setZoomFactor(numZoomFactor);
return window.storage.put('zoomFactor', numZoomFactor);
},
getPreferredAudioInputDevice: () =>
window.storage.get('preferred-audio-input-device'),
setPreferredAudioInputDevice: device =>
window.storage.put('preferred-audio-input-device', device),
getPreferredAudioOutputDevice: () =>
window.storage.get('preferred-audio-output-device'),
setPreferredAudioOutputDevice: device =>
window.storage.put('preferred-audio-output-device', device),
getPreferredVideoInputDevice: () =>
window.storage.get('preferred-video-input-device'),
setPreferredVideoInputDevice: device =>
window.storage.put('preferred-video-input-device', device),
// Chat Color redux hookups
getCustomColors: () => {
return getCustomColors(window.reduxStore.getState()) || {};
},
getConversationsWithCustomColor: colorId => {
return getConversationsWithCustomColorSelector(
window.reduxStore.getState()
)(colorId);
},
addCustomColor: (...args) =>
window.reduxActions.items.addCustomColor(...args),
editCustomColor: (...args) =>
window.reduxActions.items.editCustomColor(...args),
removeCustomColor: colorId =>
window.reduxActions.items.removeCustomColor(colorId),
removeCustomColorOnConversations: colorId =>
window.reduxActions.conversations.removeCustomColorOnConversations(
colorId
),
resetAllChatColors: () =>
window.reduxActions.conversations.resetAllChatColors(),
resetDefaultChatColor: () =>
window.reduxActions.items.resetDefaultChatColor(),
setGlobalDefaultConversationColor: (...args) =>
window.reduxActions.items.setGlobalDefaultConversationColor(...args),
// Getters only
getAvailableIODevices: async () => {
const {
availableCameras,
availableMicrophones,
availableSpeakers,
} = await calling.getAvailableIODevices();
return {
// mapping it to a pojo so that it is IPC friendly
availableCameras: availableCameras.map(
(inputDeviceInfo: MediaDeviceInfo) => ({
deviceId: inputDeviceInfo.deviceId,
groupId: inputDeviceInfo.groupId,
kind: inputDeviceInfo.kind,
label: inputDeviceInfo.label,
})
),
availableMicrophones,
availableSpeakers,
};
},
getBlockedCount: () =>
window.storage.blocked.getBlockedUuids().length +
window.storage.blocked.getBlockedGroups().length,
getDefaultConversationColor: () =>
window.storage.get(
'defaultConversationColor',
DEFAULT_CONVERSATION_COLOR
),
getLinkPreviewSetting: () => window.storage.get('linkPreviews', false),
getPhoneNumberDiscoverabilitySetting: () =>
window.storage.get(
'phoneNumberDiscoverability',
PhoneNumberDiscoverability.NotDiscoverable
),
getPhoneNumberSharingSetting: () =>
window.storage.get(
'phoneNumberSharingMode',
PhoneNumberSharingMode.Nobody
),
getReadReceiptSetting: () =>
window.storage.get('read-receipt-setting', false),
getTypingIndicatorSetting: () =>
window.storage.get('typingIndicators', false),
// Configurable settings
getAutoDownloadUpdate: () =>
window.storage.get('auto-download-update', true),
setAutoDownloadUpdate: value =>
window.storage.put('auto-download-update', value),
2021-08-18 20:08:14 +00:00
getThemeSetting: () =>
window.storage.get(
'theme-setting',
window.platform === 'darwin' ? 'system' : 'light'
),
setThemeSetting: value => {
const promise = window.storage.put('theme-setting', value);
themeChanged();
return promise;
},
getHideMenuBar: () => window.storage.get('hide-menu-bar'),
setHideMenuBar: value => {
const promise = window.storage.put('hide-menu-bar', value);
window.setAutoHideMenuBar(value);
window.setMenuBarVisibility(!value);
return promise;
},
getSystemTraySetting: () =>
parseSystemTraySetting(window.storage.get('system-tray-setting')),
setSystemTraySetting: value => {
const promise = window.storage.put('system-tray-setting', value);
window.updateSystemTraySetting(value);
return promise;
},
getNotificationSetting: () =>
window.storage.get('notification-setting', 'message'),
setNotificationSetting: (value: 'message' | 'name' | 'count' | 'off') =>
window.storage.put('notification-setting', value),
getNotificationDrawAttention: () =>
window.storage.get('notification-draw-attention', true),
setNotificationDrawAttention: value =>
window.storage.put('notification-draw-attention', value),
getAudioNotification: () => window.storage.get('audio-notification'),
setAudioNotification: value =>
window.storage.put('audio-notification', value),
getCountMutedConversations: () =>
window.storage.get('badge-count-muted-conversations', false),
setCountMutedConversations: value => {
const promise = window.storage.put(
'badge-count-muted-conversations',
value
);
window.Whisper.events.trigger('updateUnreadCount');
return promise;
},
getCallRingtoneNotification: () =>
window.storage.get('call-ringtone-notification', true),
setCallRingtoneNotification: value =>
window.storage.put('call-ringtone-notification', value),
getCallSystemNotification: () =>
window.storage.get('call-system-notification', true),
setCallSystemNotification: value =>
window.storage.put('call-system-notification', value),
getIncomingCallNotification: () =>
window.storage.get('incoming-call-notification', true),
setIncomingCallNotification: value =>
window.storage.put('incoming-call-notification', value),
getSpellCheck: () => window.storage.get('spell-check', true),
setSpellCheck: value => window.storage.put('spell-check', value),
getAlwaysRelayCalls: () => window.storage.get('always-relay-calls'),
setAlwaysRelayCalls: value =>
window.storage.put('always-relay-calls', value),
getAutoLaunch: () => window.getAutoLaunch(),
setAutoLaunch: async (value: boolean) => {
window.setAutoLaunch(value);
},
2021-08-19 01:04:38 +00:00
isPhoneNumberSharingEnabled: () => isPhoneNumberSharingEnabled(),
2021-08-18 20:08:14 +00:00
isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1,
syncRequest: () =>
new Promise<void>((resolve, reject) => {
const FIVE_MINUTES = 5 * durations.MINUTE;
2021-08-18 20:08:14 +00:00
const syncRequest = window.getSyncRequest(FIVE_MINUTES);
syncRequest.addEventListener('success', () => resolve());
syncRequest.addEventListener('timeout', () =>
reject(new Error('timeout'))
);
}),
getLastSyncTime: () => window.storage.get('synced_at'),
setLastSyncTime: value => window.storage.put('synced_at', value),
getUniversalExpireTimer: () => universalExpireTimer.get(),
setUniversalExpireTimer: async newValue => {
await universalExpireTimer.set(newValue);
// Update account in Storage Service
const conversationId = window.ConversationController.getOurConversationIdOrThrow();
const account = window.ConversationController.get(conversationId);
assert(account, "Account wasn't found");
account.captureChange('universalExpireTimer');
// Add a notification to the currently open conversation
const state = window.reduxStore.getState();
const selectedId = state.conversations.selectedConversationId;
if (selectedId) {
const conversation = window.ConversationController.get(selectedId);
assert(conversation, "Conversation wasn't found");
await conversation.updateLastMessage();
}
},
addDarkOverlay: () => {
if ($('.dark-overlay').length) {
return;
}
$(document.body).prepend('<div class="dark-overlay"></div>');
$('.dark-overlay').on('click', () => $('.dark-overlay').remove());
},
removeDarkOverlay: () => $('.dark-overlay').remove(),
showKeyboardShortcuts: () => window.showKeyboardShortcuts(),
deleteAllData: async () => {
await window.sqlInitializer.goBackToMainProcess();
2021-08-24 20:57:34 +00:00
renderClearingDataView();
2021-08-18 20:08:14 +00:00
},
showStickerPack: (packId, key) => {
// We can get these events even if the user has never linked this instance.
if (!window.Signal.Util.Registration.everDone()) {
window.log.warn('showStickerPack: Not registered, returning early');
return;
}
if (window.isShowingModal) {
window.log.warn(
'showStickerPack: Already showing modal, returning early'
);
return;
}
try {
window.isShowingModal = true;
// Kick off the download
Stickers.downloadEphemeralPack(packId, key);
const props = {
packId,
onClose: async () => {
window.isShowingModal = false;
stickerPreviewModalView.remove();
await Stickers.removeEphemeralPack(packId);
},
};
const stickerPreviewModalView = new window.Whisper.ReactWrapperView({
className: 'sticker-preview-modal-wrapper',
JSX: window.Signal.State.Roots.createStickerPreviewModal(
window.reduxStore,
props
),
});
} catch (error) {
window.isShowingModal = false;
window.log.error(
'showStickerPack: Ran into an error!',
error && error.stack ? error.stack : error
);
const errorView = new window.Whisper.ReactWrapperView({
className: 'error-modal-wrapper',
Component: window.Signal.Components.ErrorModal,
props: {
onClose: () => {
errorView.remove();
},
},
});
}
},
showGroupViaLink: async hash => {
// We can get these events even if the user has never linked this instance.
if (!window.Signal.Util.Registration.everDone()) {
window.log.warn('showGroupViaLink: Not registered, returning early');
return;
}
if (window.isShowingModal) {
window.log.warn(
'showGroupViaLink: Already showing modal, returning early'
);
return;
}
try {
await window.Signal.Groups.joinViaLink(hash);
} catch (error) {
window.log.error(
'showGroupViaLink: Ran into an error!',
error && error.stack ? error.stack : error
);
const errorView = new window.Whisper.ReactWrapperView({
className: 'error-modal-wrapper',
Component: window.Signal.Components.ErrorModal,
props: {
title: window.i18n('GroupV2--join--general-join-failure--title'),
description: window.i18n('GroupV2--join--general-join-failure'),
onClose: () => {
errorView.remove();
},
},
});
}
window.isShowingModal = false;
},
2021-08-28 13:27:38 +00:00
showConversationViaSignalDotMe(hash: string) {
if (!window.Signal.Util.Registration.everDone()) {
window.log.info(
'showConversationViaSignalDotMe: Not registered, returning early'
);
return;
}
const maybeE164 = parseE164FromSignalDotMeHash(hash);
if (maybeE164) {
trigger('showConversation', maybeE164);
return;
}
window.log.info('showConversationViaSignalDotMe: invalid E164');
if (window.isShowingModal) {
window.log.info(
'showConversationViaSignalDotMe: a modal is already showing. Doing nothing'
);
} else {
showUnknownSgnlLinkModal();
}
},
2021-08-18 20:08:14 +00:00
unknownSignalLink: () => {
window.log.warn('unknownSignalLink: Showing error dialog');
2021-08-28 13:27:38 +00:00
showUnknownSgnlLinkModal();
2021-08-18 20:08:14 +00:00
},
installStickerPack: async (packId, key) => {
Stickers.downloadStickerPack(packId, key, {
finalStatus: 'installed',
});
},
shutdown: () => Promise.resolve(),
getMediaPermissions: window.getMediaPermissions,
getMediaCameraPermissions: window.getMediaCameraPermissions,
...overrideEvents,
};
}
2021-08-28 13:27:38 +00:00
function showUnknownSgnlLinkModal(): void {
const errorView = new window.Whisper.ReactWrapperView({
className: 'error-modal-wrapper',
Component: window.Signal.Components.ErrorModal,
props: {
description: window.i18n('unknown-sgnl-link'),
onClose: () => {
errorView.remove();
},
},
});
}