Adds a pop and whoosh sound for message receive/sent
This commit is contained in:
parent
c7a430f375
commit
272b81c7cf
20 changed files with 141 additions and 145 deletions
|
@ -10434,6 +10434,14 @@
|
||||||
"messageformat": "Group Avatar",
|
"messageformat": "Group Avatar",
|
||||||
"description": "Title for the avatar picker in the group creation flow"
|
"description": "Title for the avatar picker in the group creation flow"
|
||||||
},
|
},
|
||||||
|
"icu:Preferences__message-audio-title": {
|
||||||
|
"messageformat": "In-chat message sound",
|
||||||
|
"description": "Title for message audio setting"
|
||||||
|
},
|
||||||
|
"icu:Preferences__message-audio-description": {
|
||||||
|
"messageformat": "Hear a notification sound for sent and received messages while in the chat.",
|
||||||
|
"description": "Description for message audio setting"
|
||||||
|
},
|
||||||
"Preferences__button--general": {
|
"Preferences__button--general": {
|
||||||
"message": "General",
|
"message": "General",
|
||||||
"description": "(deleted 03/29/2023) Button to switch the settings view"
|
"description": "(deleted 03/29/2023) Button to switch the settings view"
|
||||||
|
|
Binary file not shown.
BIN
sounds/pop.wav
Normal file
BIN
sounds/pop.wav
Normal file
Binary file not shown.
BIN
sounds/whoosh.wav
Normal file
BIN
sounds/whoosh.wav
Normal file
Binary file not shown.
|
@ -81,6 +81,7 @@ const getDefaultArgs = (): PropsDataType => ({
|
||||||
hasLinkPreviews: true,
|
hasLinkPreviews: true,
|
||||||
hasMediaCameraPermissions: true,
|
hasMediaCameraPermissions: true,
|
||||||
hasMediaPermissions: true,
|
hasMediaPermissions: true,
|
||||||
|
hasMessageAudio: true,
|
||||||
hasMinimizeToAndStartInSystemTray: true,
|
hasMinimizeToAndStartInSystemTray: true,
|
||||||
hasMinimizeToSystemTray: true,
|
hasMinimizeToSystemTray: true,
|
||||||
hasNotificationAttention: false,
|
hasNotificationAttention: false,
|
||||||
|
@ -92,7 +93,6 @@ const getDefaultArgs = (): PropsDataType => ({
|
||||||
hasTextFormatting: true,
|
hasTextFormatting: true,
|
||||||
hasTypingIndicators: true,
|
hasTypingIndicators: true,
|
||||||
initialSpellCheckSetting: true,
|
initialSpellCheckSetting: true,
|
||||||
isAudioNotificationsSupported: true,
|
|
||||||
isAutoDownloadUpdatesSupported: true,
|
isAutoDownloadUpdatesSupported: true,
|
||||||
isAutoLaunchSupported: true,
|
isAutoLaunchSupported: true,
|
||||||
isFormattingFlagEnabled: true,
|
isFormattingFlagEnabled: true,
|
||||||
|
@ -152,6 +152,7 @@ export default {
|
||||||
onLastSyncTimeChange: { action: true },
|
onLastSyncTimeChange: { action: true },
|
||||||
onMediaCameraPermissionsChange: { action: true },
|
onMediaCameraPermissionsChange: { action: true },
|
||||||
onMediaPermissionsChange: { action: true },
|
onMediaPermissionsChange: { action: true },
|
||||||
|
onMessageAudioChange: { action: true },
|
||||||
onMinimizeToAndStartInSystemTrayChange: { action: true },
|
onMinimizeToAndStartInSystemTrayChange: { action: true },
|
||||||
onMinimizeToSystemTrayChange: { action: true },
|
onMinimizeToSystemTrayChange: { action: true },
|
||||||
onNotificationAttentionChange: { action: true },
|
onNotificationAttentionChange: { action: true },
|
||||||
|
|
|
@ -80,6 +80,7 @@ export type PropsDataType = {
|
||||||
hasLinkPreviews: boolean;
|
hasLinkPreviews: boolean;
|
||||||
hasMediaCameraPermissions: boolean;
|
hasMediaCameraPermissions: boolean;
|
||||||
hasMediaPermissions: boolean;
|
hasMediaPermissions: boolean;
|
||||||
|
hasMessageAudio: boolean;
|
||||||
hasMinimizeToAndStartInSystemTray: boolean;
|
hasMinimizeToAndStartInSystemTray: boolean;
|
||||||
hasMinimizeToSystemTray: boolean;
|
hasMinimizeToSystemTray: boolean;
|
||||||
hasNotificationAttention: boolean;
|
hasNotificationAttention: boolean;
|
||||||
|
@ -111,7 +112,6 @@ export type PropsDataType = {
|
||||||
isFormattingFlagEnabled: boolean;
|
isFormattingFlagEnabled: boolean;
|
||||||
|
|
||||||
// Limited support features
|
// Limited support features
|
||||||
isAudioNotificationsSupported: boolean;
|
|
||||||
isAutoDownloadUpdatesSupported: boolean;
|
isAutoDownloadUpdatesSupported: boolean;
|
||||||
isAutoLaunchSupported: boolean;
|
isAutoLaunchSupported: boolean;
|
||||||
isHideMenuBarSupported: boolean;
|
isHideMenuBarSupported: boolean;
|
||||||
|
@ -163,6 +163,7 @@ type PropsFunctionType = {
|
||||||
onLastSyncTimeChange: (time: number) => unknown;
|
onLastSyncTimeChange: (time: number) => unknown;
|
||||||
onMediaCameraPermissionsChange: CheckboxChangeHandlerType;
|
onMediaCameraPermissionsChange: CheckboxChangeHandlerType;
|
||||||
onMediaPermissionsChange: CheckboxChangeHandlerType;
|
onMediaPermissionsChange: CheckboxChangeHandlerType;
|
||||||
|
onMessageAudioChange: CheckboxChangeHandlerType;
|
||||||
onMinimizeToAndStartInSystemTrayChange: CheckboxChangeHandlerType;
|
onMinimizeToAndStartInSystemTrayChange: CheckboxChangeHandlerType;
|
||||||
onMinimizeToSystemTrayChange: CheckboxChangeHandlerType;
|
onMinimizeToSystemTrayChange: CheckboxChangeHandlerType;
|
||||||
onNotificationAttentionChange: CheckboxChangeHandlerType;
|
onNotificationAttentionChange: CheckboxChangeHandlerType;
|
||||||
|
@ -252,6 +253,7 @@ export function Preferences({
|
||||||
hasLinkPreviews,
|
hasLinkPreviews,
|
||||||
hasMediaCameraPermissions,
|
hasMediaCameraPermissions,
|
||||||
hasMediaPermissions,
|
hasMediaPermissions,
|
||||||
|
hasMessageAudio,
|
||||||
hasMinimizeToAndStartInSystemTray,
|
hasMinimizeToAndStartInSystemTray,
|
||||||
hasMinimizeToSystemTray,
|
hasMinimizeToSystemTray,
|
||||||
hasNotificationAttention,
|
hasNotificationAttention,
|
||||||
|
@ -264,7 +266,6 @@ export function Preferences({
|
||||||
hasTypingIndicators,
|
hasTypingIndicators,
|
||||||
i18n,
|
i18n,
|
||||||
initialSpellCheckSetting,
|
initialSpellCheckSetting,
|
||||||
isAudioNotificationsSupported,
|
|
||||||
isAutoDownloadUpdatesSupported,
|
isAutoDownloadUpdatesSupported,
|
||||||
isAutoLaunchSupported,
|
isAutoLaunchSupported,
|
||||||
isFormattingFlagEnabled,
|
isFormattingFlagEnabled,
|
||||||
|
@ -290,6 +291,7 @@ export function Preferences({
|
||||||
onLastSyncTimeChange,
|
onLastSyncTimeChange,
|
||||||
onMediaCameraPermissionsChange,
|
onMediaCameraPermissionsChange,
|
||||||
onMediaPermissionsChange,
|
onMediaPermissionsChange,
|
||||||
|
onMessageAudioChange,
|
||||||
onMinimizeToAndStartInSystemTrayChange,
|
onMinimizeToAndStartInSystemTrayChange,
|
||||||
onMinimizeToSystemTrayChange,
|
onMinimizeToSystemTrayChange,
|
||||||
onNotificationAttentionChange,
|
onNotificationAttentionChange,
|
||||||
|
@ -857,15 +859,6 @@ export function Preferences({
|
||||||
onChange={onNotificationAttentionChange}
|
onChange={onNotificationAttentionChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isAudioNotificationsSupported && (
|
|
||||||
<Checkbox
|
|
||||||
checked={hasAudioNotifications}
|
|
||||||
label={i18n('icu:audioNotificationDescription')}
|
|
||||||
moduleClassName="Preferences__checkbox"
|
|
||||||
name="audioNotification"
|
|
||||||
onChange={onAudioNotificationsChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={hasCountMutedConversations}
|
checked={hasCountMutedConversations}
|
||||||
label={i18n('icu:countMutedConversationsDescription')}
|
label={i18n('icu:countMutedConversationsDescription')}
|
||||||
|
@ -901,6 +894,24 @@ export function Preferences({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
<SettingsRow>
|
||||||
|
<Checkbox
|
||||||
|
checked={hasAudioNotifications}
|
||||||
|
label={i18n('icu:audioNotificationDescription')}
|
||||||
|
moduleClassName="Preferences__checkbox"
|
||||||
|
name="audioNotification"
|
||||||
|
onChange={onAudioNotificationsChange}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={hasMessageAudio}
|
||||||
|
description={i18n('icu:Preferences__message-audio-description')}
|
||||||
|
disabled={!hasAudioNotifications}
|
||||||
|
label={i18n('icu:Preferences__message-audio-title')}
|
||||||
|
moduleClassName="Preferences__checkbox"
|
||||||
|
name="messageAudio"
|
||||||
|
onChange={onMessageAudioChange}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else if (page === Page.Privacy) {
|
} else if (page === Page.Privacy) {
|
||||||
|
|
|
@ -81,6 +81,7 @@ export class SettingsChannel extends EventEmitter {
|
||||||
|
|
||||||
this.installSetting('notificationSetting');
|
this.installSetting('notificationSetting');
|
||||||
this.installSetting('notificationDrawAttention');
|
this.installSetting('notificationDrawAttention');
|
||||||
|
this.installSetting('audioMessage');
|
||||||
this.installSetting('audioNotification');
|
this.installSetting('audioNotification');
|
||||||
this.installSetting('countMutedConversations');
|
this.installSetting('countMutedConversations');
|
||||||
|
|
||||||
|
|
|
@ -4,32 +4,30 @@
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { Sound } from '../util/Sound';
|
import { Sound, SoundType } from '../util/Sound';
|
||||||
import {
|
import { shouldHideExpiringMessageBody } from '../types/Settings';
|
||||||
AudioNotificationSupport,
|
|
||||||
getAudioNotificationSupport,
|
|
||||||
shouldHideExpiringMessageBody,
|
|
||||||
} from '../types/Settings';
|
|
||||||
import OS from '../util/os/osMain';
|
import OS from '../util/os/osMain';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { makeEnumParser } from '../util/enum';
|
import { makeEnumParser } from '../util/enum';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import type { StorageInterface } from '../types/Storage.d';
|
import type { StorageInterface } from '../types/Storage.d';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import { drop } from '../util/drop';
|
||||||
|
|
||||||
type NotificationDataType = Readonly<{
|
type NotificationDataType = Readonly<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
storyId?: string;
|
isExpiringMessage: boolean;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
senderTitle: string;
|
|
||||||
message: string;
|
message: string;
|
||||||
notificationIconUrl?: undefined | string;
|
notificationIconUrl?: undefined | string;
|
||||||
isExpiringMessage: boolean;
|
|
||||||
reaction?: {
|
reaction?: {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
targetAuthorUuid: string;
|
targetAuthorUuid: string;
|
||||||
targetTimestamp: number;
|
targetTimestamp: number;
|
||||||
};
|
};
|
||||||
|
senderTitle: string;
|
||||||
|
storyId?: string;
|
||||||
|
useTriToneSound?: boolean;
|
||||||
wasShown?: boolean;
|
wasShown?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -133,6 +131,7 @@ class NotificationService extends EventEmitter {
|
||||||
onNotificationClick,
|
onNotificationClick,
|
||||||
silent,
|
silent,
|
||||||
title,
|
title,
|
||||||
|
useTriToneSound,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
icon?: string;
|
icon?: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
@ -140,28 +139,25 @@ class NotificationService extends EventEmitter {
|
||||||
onNotificationClick: () => void;
|
onNotificationClick: () => void;
|
||||||
silent: boolean;
|
silent: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
useTriToneSound?: boolean;
|
||||||
}>): void {
|
}>): void {
|
||||||
log.info('NotificationService: showing a notification');
|
log.info('NotificationService: showing a notification');
|
||||||
|
|
||||||
this.lastNotification?.close();
|
this.lastNotification?.close();
|
||||||
|
|
||||||
const audioNotificationSupport = getAudioNotificationSupport(OS);
|
|
||||||
|
|
||||||
const notification = new window.Notification(title, {
|
const notification = new window.Notification(title, {
|
||||||
body: OS.isLinux() ? filterNotificationText(message) : message,
|
body: OS.isLinux() ? filterNotificationText(message) : message,
|
||||||
icon,
|
icon,
|
||||||
silent:
|
silent: true,
|
||||||
silent || audioNotificationSupport !== AudioNotificationSupport.Native,
|
|
||||||
tag: messageId,
|
tag: messageId,
|
||||||
});
|
});
|
||||||
notification.onclick = onNotificationClick;
|
notification.onclick = onNotificationClick;
|
||||||
|
|
||||||
if (
|
if (!silent) {
|
||||||
!silent &&
|
const soundType =
|
||||||
audioNotificationSupport === AudioNotificationSupport.Custom
|
messageId && !useTriToneSound ? SoundType.Pop : SoundType.TriTone;
|
||||||
) {
|
|
||||||
// We kick off the sound to be played. No need to await it.
|
// We kick off the sound to be played. No need to await it.
|
||||||
void new Sound({ src: 'sounds/notification.ogg' }).play();
|
drop(new Sound({ soundType }).play());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastNotification = notification;
|
this.lastNotification = notification;
|
||||||
|
@ -273,12 +269,13 @@ class NotificationService extends EventEmitter {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
conversationId,
|
conversationId,
|
||||||
storyId,
|
|
||||||
messageId,
|
|
||||||
senderTitle,
|
|
||||||
message,
|
|
||||||
isExpiringMessage,
|
isExpiringMessage,
|
||||||
|
message,
|
||||||
|
messageId,
|
||||||
reaction,
|
reaction,
|
||||||
|
senderTitle,
|
||||||
|
storyId,
|
||||||
|
useTriToneSound,
|
||||||
wasShown,
|
wasShown,
|
||||||
} = notificationData;
|
} = notificationData;
|
||||||
|
|
||||||
|
@ -346,13 +343,15 @@ class NotificationService extends EventEmitter {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.notify({
|
this.notify({
|
||||||
title: notificationTitle,
|
|
||||||
icon: notificationIconUrl,
|
icon: notificationIconUrl,
|
||||||
|
messageId,
|
||||||
message: notificationMessage,
|
message: notificationMessage,
|
||||||
silent: !shouldPlayNotificationSound,
|
|
||||||
onNotificationClick: () => {
|
onNotificationClick: () => {
|
||||||
this.emit('click', conversationId, messageId, storyId);
|
this.emit('click', conversationId, messageId, storyId);
|
||||||
},
|
},
|
||||||
|
silent: !shouldPlayNotificationSound,
|
||||||
|
title: notificationTitle,
|
||||||
|
useTriToneSound,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,7 @@ import { strictAssert } from '../../util/assert';
|
||||||
import { makeQuote } from '../../util/makeQuote';
|
import { makeQuote } from '../../util/makeQuote';
|
||||||
import { sendEditedMessage as doSendEditedMessage } from '../../util/sendEditedMessage';
|
import { sendEditedMessage as doSendEditedMessage } from '../../util/sendEditedMessage';
|
||||||
import { maybeBlockSendForFormattingModal } from '../../util/maybeBlockSendForFormattingModal';
|
import { maybeBlockSendForFormattingModal } from '../../util/maybeBlockSendForFormattingModal';
|
||||||
|
import { Sound, SoundType } from '../../util/Sound';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||||
|
@ -616,6 +617,8 @@ function sendMultiMediaMessage(
|
||||||
);
|
);
|
||||||
dispatch(incrementSendCounter(conversationId));
|
dispatch(incrementSendCounter(conversationId));
|
||||||
dispatch(setComposerDisabledState(conversationId, false));
|
dispatch(setComposerDisabledState(conversationId, false));
|
||||||
|
|
||||||
|
drop(new Sound({ soundType: SoundType.Whoosh }).play());
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -89,7 +89,8 @@ async function notifyForCall(
|
||||||
onNotificationClick: () => {
|
onNotificationClick: () => {
|
||||||
window.IPC.showWindow();
|
window.IPC.showWindow();
|
||||||
},
|
},
|
||||||
silent: false,
|
// The ringtone plays so we don't need sound for the notification
|
||||||
|
silent: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
import { globalMessageAudio } from '../../services/globalMessageAudio';
|
import { globalMessageAudio } from '../../services/globalMessageAudio';
|
||||||
import { strictAssert } from '../../util/assert';
|
import { strictAssert } from '../../util/assert';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { Sound } from '../../util/Sound';
|
import { Sound, SoundType } from '../../util/Sound';
|
||||||
import { getConversations } from '../selectors/conversations';
|
import { getConversations } from '../selectors/conversations';
|
||||||
import { SeenStatus } from '../../MessageSeenStatus';
|
import { SeenStatus } from '../../MessageSeenStatus';
|
||||||
import { markViewed } from '../ducks/conversations';
|
import { markViewed } from '../ducks/conversations';
|
||||||
|
@ -21,10 +21,10 @@ import * as Errors from '../../types/errors';
|
||||||
import { usePrevious } from '../../hooks/usePrevious';
|
import { usePrevious } from '../../hooks/usePrevious';
|
||||||
|
|
||||||
const stateChangeConfirmUpSound = new Sound({
|
const stateChangeConfirmUpSound = new Sound({
|
||||||
src: 'sounds/state-change_confirm-up.ogg',
|
soundType: SoundType.VoiceNoteStart,
|
||||||
});
|
});
|
||||||
const stateChangeConfirmDownSound = new Sound({
|
const stateChangeConfirmDownSound = new Sound({
|
||||||
src: 'sounds/state-change_confirm-down.ogg',
|
soundType: SoundType.VoiceNoteEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,74 +19,6 @@ describe('Settings', () => {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getAudioNotificationSupport', () => {
|
|
||||||
it('returns native support on macOS', () => {
|
|
||||||
sandbox.stub(process, 'platform').value('darwin');
|
|
||||||
const OS = getOSFunctions(os.release());
|
|
||||||
assert.strictEqual(
|
|
||||||
Settings.getAudioNotificationSupport(OS),
|
|
||||||
Settings.AudioNotificationSupport.Native
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns no support on Windows 7', () => {
|
|
||||||
sandbox.stub(process, 'platform').value('win32');
|
|
||||||
sandbox.stub(os, 'release').returns('7.0.0');
|
|
||||||
const OS = getOSFunctions(os.release());
|
|
||||||
assert.strictEqual(
|
|
||||||
Settings.getAudioNotificationSupport(OS),
|
|
||||||
Settings.AudioNotificationSupport.None
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns native support on Windows 8', () => {
|
|
||||||
sandbox.stub(process, 'platform').value('win32');
|
|
||||||
sandbox.stub(os, 'release').returns('8.0.0');
|
|
||||||
const OS = getOSFunctions(os.release());
|
|
||||||
assert.strictEqual(
|
|
||||||
Settings.getAudioNotificationSupport(OS),
|
|
||||||
Settings.AudioNotificationSupport.Native
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns custom support on Linux', () => {
|
|
||||||
sandbox.stub(process, 'platform').value('linux');
|
|
||||||
const OS = getOSFunctions(os.release());
|
|
||||||
assert.strictEqual(
|
|
||||||
Settings.getAudioNotificationSupport(OS),
|
|
||||||
Settings.AudioNotificationSupport.Custom
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isAudioNotificationSupported', () => {
|
|
||||||
it('returns true on macOS', () => {
|
|
||||||
sandbox.stub(process, 'platform').value('darwin');
|
|
||||||
const OS = getOSFunctions(os.release());
|
|
||||||
assert.isTrue(Settings.isAudioNotificationSupported(OS));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false on Windows 7', () => {
|
|
||||||
sandbox.stub(process, 'platform').value('win32');
|
|
||||||
sandbox.stub(os, 'release').returns('7.0.0');
|
|
||||||
const OS = getOSFunctions(os.release());
|
|
||||||
assert.isFalse(Settings.isAudioNotificationSupported(OS));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true on Windows 8', () => {
|
|
||||||
sandbox.stub(process, 'platform').value('win32');
|
|
||||||
sandbox.stub(os, 'release').returns('8.0.0');
|
|
||||||
const OS = getOSFunctions(os.release());
|
|
||||||
assert.isTrue(Settings.isAudioNotificationSupported(OS));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true on Linux', () => {
|
|
||||||
sandbox.stub(process, 'platform').value('linux');
|
|
||||||
const OS = getOSFunctions(os.release());
|
|
||||||
assert.isTrue(Settings.isAudioNotificationSupported(OS));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isNotificationGroupingSupported', () => {
|
describe('isNotificationGroupingSupported', () => {
|
||||||
it('returns true on macOS', () => {
|
it('returns true on macOS', () => {
|
||||||
sandbox.stub(process, 'platform').value('darwin');
|
sandbox.stub(process, 'platform').value('darwin');
|
||||||
|
|
|
@ -8,27 +8,6 @@ import { isProduction } from '../util/version';
|
||||||
|
|
||||||
const MIN_WINDOWS_VERSION = '8.0.0';
|
const MIN_WINDOWS_VERSION = '8.0.0';
|
||||||
|
|
||||||
export enum AudioNotificationSupport {
|
|
||||||
None,
|
|
||||||
Native,
|
|
||||||
Custom,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAudioNotificationSupport(
|
|
||||||
OS: OSType
|
|
||||||
): AudioNotificationSupport {
|
|
||||||
if (OS.isWindows(MIN_WINDOWS_VERSION) || OS.isMacOS()) {
|
|
||||||
return AudioNotificationSupport.Native;
|
|
||||||
}
|
|
||||||
if (OS.isLinux()) {
|
|
||||||
return AudioNotificationSupport.Custom;
|
|
||||||
}
|
|
||||||
return AudioNotificationSupport.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isAudioNotificationSupported = (OS: OSType): boolean =>
|
|
||||||
getAudioNotificationSupport(OS) !== AudioNotificationSupport.None;
|
|
||||||
|
|
||||||
// Using `Notification::tag` has a bug on Windows 7:
|
// Using `Notification::tag` has a bug on Windows 7:
|
||||||
// https://github.com/electron/electron/issues/11189
|
// https://github.com/electron/electron/issues/11189
|
||||||
export const isNotificationGroupingSupported = (OS: OSType): boolean =>
|
export const isNotificationGroupingSupported = (OS: OSType): boolean =>
|
||||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -62,6 +62,7 @@ export type StorageAccessType = {
|
||||||
'spell-check': boolean;
|
'spell-check': boolean;
|
||||||
'system-tray-setting': SystemTraySetting;
|
'system-tray-setting': SystemTraySetting;
|
||||||
'theme-setting': ThemeSettingType;
|
'theme-setting': ThemeSettingType;
|
||||||
|
audioMessage: boolean;
|
||||||
attachmentMigration_isComplete: boolean;
|
attachmentMigration_isComplete: boolean;
|
||||||
attachmentMigration_lastProcessedIndex: number;
|
attachmentMigration_lastProcessedIndex: number;
|
||||||
blocked: ReadonlyArray<string>;
|
blocked: ReadonlyArray<string>;
|
||||||
|
|
|
@ -2,14 +2,26 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
import { missingCaseError } from './missingCaseError';
|
||||||
|
|
||||||
|
export enum SoundType {
|
||||||
|
CallingHangUp,
|
||||||
|
CallingPresenting,
|
||||||
|
Pop,
|
||||||
|
Ringtone,
|
||||||
|
TriTone,
|
||||||
|
VoiceNoteEnd,
|
||||||
|
VoiceNoteStart,
|
||||||
|
Whoosh,
|
||||||
|
}
|
||||||
|
|
||||||
export type SoundOpts = {
|
export type SoundOpts = {
|
||||||
loop?: boolean;
|
loop?: boolean;
|
||||||
src: string;
|
soundType: SoundType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Sound {
|
export class Sound {
|
||||||
static sounds = new Map();
|
static sounds = new Map<SoundType, AudioBuffer>();
|
||||||
|
|
||||||
private static context: AudioContext | undefined;
|
private static context: AudioContext | undefined;
|
||||||
|
|
||||||
|
@ -17,27 +29,29 @@ export class Sound {
|
||||||
|
|
||||||
private node?: AudioBufferSourceNode;
|
private node?: AudioBufferSourceNode;
|
||||||
|
|
||||||
private readonly src: string;
|
private readonly soundType: SoundType;
|
||||||
|
|
||||||
constructor(options: SoundOpts) {
|
constructor(options: SoundOpts) {
|
||||||
this.loop = Boolean(options.loop);
|
this.loop = Boolean(options.loop);
|
||||||
this.src = options.src;
|
this.soundType = options.soundType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async play(): Promise<void> {
|
async play(): Promise<void> {
|
||||||
if (!Sound.sounds.has(this.src)) {
|
let soundBuffer = Sound.sounds.get(this.soundType);
|
||||||
|
|
||||||
|
if (!soundBuffer) {
|
||||||
try {
|
try {
|
||||||
const buffer = await Sound.loadSoundFile(this.src);
|
const src = Sound.getSrc(this.soundType);
|
||||||
|
const buffer = await Sound.loadSoundFile(src);
|
||||||
const decodedBuffer = await this.context.decodeAudioData(buffer);
|
const decodedBuffer = await this.context.decodeAudioData(buffer);
|
||||||
Sound.sounds.set(this.src, decodedBuffer);
|
Sound.sounds.set(this.soundType, decodedBuffer);
|
||||||
|
soundBuffer = decodedBuffer;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(`Sound error: ${err}`);
|
log.error(`Sound error: ${err}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const soundBuffer = Sound.sounds.get(this.src);
|
|
||||||
|
|
||||||
const soundNode = this.context.createBufferSource();
|
const soundNode = this.context.createBufferSource();
|
||||||
soundNode.buffer = soundBuffer;
|
soundNode.buffer = soundBuffer;
|
||||||
|
|
||||||
|
@ -87,4 +101,40 @@ export class Sound {
|
||||||
xhr.send();
|
xhr.send();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getSrc(soundStyle: SoundType): string {
|
||||||
|
if (soundStyle === SoundType.CallingHangUp) {
|
||||||
|
return 'sounds/navigation-cancel.ogg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soundStyle === SoundType.CallingPresenting) {
|
||||||
|
return 'sounds/navigation_selection-complete-celebration.ogg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soundStyle === SoundType.Pop) {
|
||||||
|
return 'sounds/pop.wav';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soundStyle === SoundType.TriTone) {
|
||||||
|
return 'sounds/notification.ogg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soundStyle === SoundType.Ringtone) {
|
||||||
|
return 'sounds/ringtone_minimal.ogg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soundStyle === SoundType.VoiceNoteEnd) {
|
||||||
|
return 'sounds/state-change_confirm-down.ogg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soundStyle === SoundType.VoiceNoteStart) {
|
||||||
|
return 'sounds/state-change_confirm-up.ogg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soundStyle === SoundType.Whoosh) {
|
||||||
|
return 'sounds/whoosh.wav';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw missingCaseError(soundStyle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { MINUTE } from './durations';
|
import { MINUTE } from './durations';
|
||||||
import { Sound } from './Sound';
|
import { Sound, SoundType } from './Sound';
|
||||||
|
|
||||||
const ringtoneEventQueue = new PQueue({
|
const ringtoneEventQueue = new PQueue({
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
|
@ -21,7 +21,7 @@ class CallingTones {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tone = new Sound({
|
const tone = new Sound({
|
||||||
src: 'sounds/navigation-cancel.ogg',
|
soundType: SoundType.CallingHangUp,
|
||||||
});
|
});
|
||||||
await tone.play();
|
await tone.play();
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ class CallingTones {
|
||||||
|
|
||||||
this.ringtone = new Sound({
|
this.ringtone = new Sound({
|
||||||
loop: true,
|
loop: true,
|
||||||
src: 'sounds/ringtone_minimal.ogg',
|
soundType: SoundType.Ringtone,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.ringtone.play();
|
await this.ringtone.play();
|
||||||
|
@ -63,7 +63,7 @@ class CallingTones {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tone = new Sound({
|
const tone = new Sound({
|
||||||
src: 'sounds/navigation_selection-complete-celebration.ogg',
|
soundType: SoundType.CallingPresenting,
|
||||||
});
|
});
|
||||||
|
|
||||||
await tone.play();
|
await tone.play();
|
||||||
|
|
|
@ -50,6 +50,7 @@ type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
||||||
export type IPCEventsValuesType = {
|
export type IPCEventsValuesType = {
|
||||||
alwaysRelayCalls: boolean | undefined;
|
alwaysRelayCalls: boolean | undefined;
|
||||||
audioNotification: boolean | undefined;
|
audioNotification: boolean | undefined;
|
||||||
|
audioMessage: boolean;
|
||||||
autoDownloadUpdate: boolean;
|
autoDownloadUpdate: boolean;
|
||||||
autoLaunch: boolean;
|
autoLaunch: boolean;
|
||||||
callRingtoneNotification: boolean;
|
callRingtoneNotification: boolean;
|
||||||
|
@ -371,6 +372,8 @@ export function createIPCEvents(
|
||||||
window.storage.get('notification-draw-attention', false),
|
window.storage.get('notification-draw-attention', false),
|
||||||
setNotificationDrawAttention: value =>
|
setNotificationDrawAttention: value =>
|
||||||
window.storage.put('notification-draw-attention', value),
|
window.storage.put('notification-draw-attention', value),
|
||||||
|
getAudioMessage: () => window.storage.get('audioMessage', false),
|
||||||
|
setAudioMessage: value => window.storage.put('audioMessage', value),
|
||||||
getAudioNotification: () => window.storage.get('audio-notification'),
|
getAudioNotification: () => window.storage.get('audio-notification'),
|
||||||
setAudioNotification: value =>
|
setAudioNotification: value =>
|
||||||
window.storage.put('audio-notification', value),
|
window.storage.put('audio-notification', value),
|
||||||
|
|
|
@ -38,6 +38,7 @@ installCallback('shouldShowStoriesSettings');
|
||||||
installCallback('syncRequest');
|
installCallback('syncRequest');
|
||||||
|
|
||||||
installSetting('alwaysRelayCalls');
|
installSetting('alwaysRelayCalls');
|
||||||
|
installSetting('audioMessage');
|
||||||
installSetting('audioNotification');
|
installSetting('audioNotification');
|
||||||
installSetting('autoDownloadUpdate');
|
installSetting('autoDownloadUpdate');
|
||||||
installSetting('autoLaunch');
|
installSetting('autoLaunch');
|
||||||
|
|
|
@ -44,6 +44,7 @@ SettingsWindowProps.onRender(
|
||||||
hasLinkPreviews,
|
hasLinkPreviews,
|
||||||
hasMediaCameraPermissions,
|
hasMediaCameraPermissions,
|
||||||
hasMediaPermissions,
|
hasMediaPermissions,
|
||||||
|
hasMessageAudio,
|
||||||
hasMinimizeToAndStartInSystemTray,
|
hasMinimizeToAndStartInSystemTray,
|
||||||
hasMinimizeToSystemTray,
|
hasMinimizeToSystemTray,
|
||||||
hasNotificationAttention,
|
hasNotificationAttention,
|
||||||
|
@ -55,7 +56,6 @@ SettingsWindowProps.onRender(
|
||||||
hasTextFormatting,
|
hasTextFormatting,
|
||||||
hasTypingIndicators,
|
hasTypingIndicators,
|
||||||
initialSpellCheckSetting,
|
initialSpellCheckSetting,
|
||||||
isAudioNotificationsSupported,
|
|
||||||
isAutoDownloadUpdatesSupported,
|
isAutoDownloadUpdatesSupported,
|
||||||
isAutoLaunchSupported,
|
isAutoLaunchSupported,
|
||||||
isFormattingFlagEnabled,
|
isFormattingFlagEnabled,
|
||||||
|
@ -80,6 +80,7 @@ SettingsWindowProps.onRender(
|
||||||
onLastSyncTimeChange,
|
onLastSyncTimeChange,
|
||||||
onMediaCameraPermissionsChange,
|
onMediaCameraPermissionsChange,
|
||||||
onMediaPermissionsChange,
|
onMediaPermissionsChange,
|
||||||
|
onMessageAudioChange,
|
||||||
onMinimizeToAndStartInSystemTrayChange,
|
onMinimizeToAndStartInSystemTrayChange,
|
||||||
onMinimizeToSystemTrayChange,
|
onMinimizeToSystemTrayChange,
|
||||||
onNotificationAttentionChange,
|
onNotificationAttentionChange,
|
||||||
|
@ -141,6 +142,7 @@ SettingsWindowProps.onRender(
|
||||||
hasLinkPreviews={hasLinkPreviews}
|
hasLinkPreviews={hasLinkPreviews}
|
||||||
hasMediaCameraPermissions={hasMediaCameraPermissions}
|
hasMediaCameraPermissions={hasMediaCameraPermissions}
|
||||||
hasMediaPermissions={hasMediaPermissions}
|
hasMediaPermissions={hasMediaPermissions}
|
||||||
|
hasMessageAudio={hasMessageAudio}
|
||||||
hasMinimizeToAndStartInSystemTray={hasMinimizeToAndStartInSystemTray}
|
hasMinimizeToAndStartInSystemTray={hasMinimizeToAndStartInSystemTray}
|
||||||
hasMinimizeToSystemTray={hasMinimizeToSystemTray}
|
hasMinimizeToSystemTray={hasMinimizeToSystemTray}
|
||||||
hasNotificationAttention={hasNotificationAttention}
|
hasNotificationAttention={hasNotificationAttention}
|
||||||
|
@ -153,7 +155,6 @@ SettingsWindowProps.onRender(
|
||||||
hasTypingIndicators={hasTypingIndicators}
|
hasTypingIndicators={hasTypingIndicators}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
initialSpellCheckSetting={initialSpellCheckSetting}
|
initialSpellCheckSetting={initialSpellCheckSetting}
|
||||||
isAudioNotificationsSupported={isAudioNotificationsSupported}
|
|
||||||
isAutoDownloadUpdatesSupported={isAutoDownloadUpdatesSupported}
|
isAutoDownloadUpdatesSupported={isAutoDownloadUpdatesSupported}
|
||||||
isAutoLaunchSupported={isAutoLaunchSupported}
|
isAutoLaunchSupported={isAutoLaunchSupported}
|
||||||
isFormattingFlagEnabled={isFormattingFlagEnabled}
|
isFormattingFlagEnabled={isFormattingFlagEnabled}
|
||||||
|
@ -180,6 +181,7 @@ SettingsWindowProps.onRender(
|
||||||
onLastSyncTimeChange={onLastSyncTimeChange}
|
onLastSyncTimeChange={onLastSyncTimeChange}
|
||||||
onMediaCameraPermissionsChange={onMediaCameraPermissionsChange}
|
onMediaCameraPermissionsChange={onMediaCameraPermissionsChange}
|
||||||
onMediaPermissionsChange={onMediaPermissionsChange}
|
onMediaPermissionsChange={onMediaPermissionsChange}
|
||||||
|
onMessageAudioChange={onMessageAudioChange}
|
||||||
onMinimizeToAndStartInSystemTrayChange={
|
onMinimizeToAndStartInSystemTrayChange={
|
||||||
onMinimizeToAndStartInSystemTrayChange
|
onMinimizeToAndStartInSystemTrayChange
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ function doneRendering() {
|
||||||
ipcRenderer.send('settings-done-rendering');
|
ipcRenderer.send('settings-done-rendering');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settingMessageAudio = createSetting('audioMessage');
|
||||||
const settingAudioNotification = createSetting('audioNotification');
|
const settingAudioNotification = createSetting('audioNotification');
|
||||||
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
|
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
|
||||||
const settingAutoLaunch = createSetting('autoLaunch');
|
const settingAutoLaunch = createSetting('autoLaunch');
|
||||||
|
@ -152,6 +153,7 @@ async function renderPreferences() {
|
||||||
hasLinkPreviews,
|
hasLinkPreviews,
|
||||||
hasMediaCameraPermissions,
|
hasMediaCameraPermissions,
|
||||||
hasMediaPermissions,
|
hasMediaPermissions,
|
||||||
|
hasMessageAudio,
|
||||||
hasNotificationAttention,
|
hasNotificationAttention,
|
||||||
hasReadReceipts,
|
hasReadReceipts,
|
||||||
hasRelayCalls,
|
hasRelayCalls,
|
||||||
|
@ -193,6 +195,7 @@ async function renderPreferences() {
|
||||||
hasLinkPreviews: settingLinkPreview.getValue(),
|
hasLinkPreviews: settingLinkPreview.getValue(),
|
||||||
hasMediaCameraPermissions: settingMediaCameraPermissions.getValue(),
|
hasMediaCameraPermissions: settingMediaCameraPermissions.getValue(),
|
||||||
hasMediaPermissions: settingMediaPermissions.getValue(),
|
hasMediaPermissions: settingMediaPermissions.getValue(),
|
||||||
|
hasMessageAudio: settingMessageAudio.getValue(),
|
||||||
hasNotificationAttention: settingNotificationDrawAttention.getValue(),
|
hasNotificationAttention: settingNotificationDrawAttention.getValue(),
|
||||||
hasReadReceipts: settingReadReceipts.getValue(),
|
hasReadReceipts: settingReadReceipts.getValue(),
|
||||||
hasRelayCalls: settingRelayCalls.getValue(),
|
hasRelayCalls: settingRelayCalls.getValue(),
|
||||||
|
@ -253,6 +256,7 @@ async function renderPreferences() {
|
||||||
hasLinkPreviews,
|
hasLinkPreviews,
|
||||||
hasMediaCameraPermissions,
|
hasMediaCameraPermissions,
|
||||||
hasMediaPermissions,
|
hasMediaPermissions,
|
||||||
|
hasMessageAudio,
|
||||||
hasMinimizeToAndStartInSystemTray,
|
hasMinimizeToAndStartInSystemTray,
|
||||||
hasMinimizeToSystemTray,
|
hasMinimizeToSystemTray,
|
||||||
hasNotificationAttention,
|
hasNotificationAttention,
|
||||||
|
@ -293,7 +297,6 @@ async function renderPreferences() {
|
||||||
shouldShowStoriesSettings,
|
shouldShowStoriesSettings,
|
||||||
|
|
||||||
// Limited support features
|
// Limited support features
|
||||||
isAudioNotificationsSupported: Settings.isAudioNotificationSupported(OS),
|
|
||||||
isAutoDownloadUpdatesSupported: Settings.isAutoDownloadUpdatesSupported(OS),
|
isAutoDownloadUpdatesSupported: Settings.isAutoDownloadUpdatesSupported(OS),
|
||||||
isAutoLaunchSupported: Settings.isAutoLaunchSupported(OS),
|
isAutoLaunchSupported: Settings.isAutoLaunchSupported(OS),
|
||||||
isHideMenuBarSupported: Settings.isHideMenuBarSupported(OS),
|
isHideMenuBarSupported: Settings.isHideMenuBarSupported(OS),
|
||||||
|
@ -347,6 +350,7 @@ async function renderPreferences() {
|
||||||
onMediaCameraPermissionsChange: attachRenderCallback(
|
onMediaCameraPermissionsChange: attachRenderCallback(
|
||||||
settingMediaCameraPermissions.setValue
|
settingMediaCameraPermissions.setValue
|
||||||
),
|
),
|
||||||
|
onMessageAudioChange: attachRenderCallback(settingMessageAudio.setValue),
|
||||||
onMinimizeToAndStartInSystemTrayChange: attachRenderCallback(
|
onMinimizeToAndStartInSystemTrayChange: attachRenderCallback(
|
||||||
async (value: boolean) => {
|
async (value: boolean) => {
|
||||||
await settingSystemTraySetting.setValue(
|
await settingSystemTraySetting.setValue(
|
||||||
|
|
Loading…
Reference in a new issue