Adds a pop and whoosh sound for message receive/sent

This commit is contained in:
Josh Perez 2023-05-08 15:59:36 -04:00
parent c7a430f375
commit 272b81c7cf
20 changed files with 141 additions and 145 deletions

View file

@ -10434,6 +10434,14 @@
"messageformat": "Group Avatar",
"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": {
"message": "General",
"description": "(deleted 03/29/2023) Button to switch the settings view"

Binary file not shown.

BIN
sounds/pop.wav Normal file

Binary file not shown.

BIN
sounds/whoosh.wav Normal file

Binary file not shown.

View file

@ -81,6 +81,7 @@ const getDefaultArgs = (): PropsDataType => ({
hasLinkPreviews: true,
hasMediaCameraPermissions: true,
hasMediaPermissions: true,
hasMessageAudio: true,
hasMinimizeToAndStartInSystemTray: true,
hasMinimizeToSystemTray: true,
hasNotificationAttention: false,
@ -92,7 +93,6 @@ const getDefaultArgs = (): PropsDataType => ({
hasTextFormatting: true,
hasTypingIndicators: true,
initialSpellCheckSetting: true,
isAudioNotificationsSupported: true,
isAutoDownloadUpdatesSupported: true,
isAutoLaunchSupported: true,
isFormattingFlagEnabled: true,
@ -152,6 +152,7 @@ export default {
onLastSyncTimeChange: { action: true },
onMediaCameraPermissionsChange: { action: true },
onMediaPermissionsChange: { action: true },
onMessageAudioChange: { action: true },
onMinimizeToAndStartInSystemTrayChange: { action: true },
onMinimizeToSystemTrayChange: { action: true },
onNotificationAttentionChange: { action: true },

View file

@ -80,6 +80,7 @@ export type PropsDataType = {
hasLinkPreviews: boolean;
hasMediaCameraPermissions: boolean;
hasMediaPermissions: boolean;
hasMessageAudio: boolean;
hasMinimizeToAndStartInSystemTray: boolean;
hasMinimizeToSystemTray: boolean;
hasNotificationAttention: boolean;
@ -111,7 +112,6 @@ export type PropsDataType = {
isFormattingFlagEnabled: boolean;
// Limited support features
isAudioNotificationsSupported: boolean;
isAutoDownloadUpdatesSupported: boolean;
isAutoLaunchSupported: boolean;
isHideMenuBarSupported: boolean;
@ -163,6 +163,7 @@ type PropsFunctionType = {
onLastSyncTimeChange: (time: number) => unknown;
onMediaCameraPermissionsChange: CheckboxChangeHandlerType;
onMediaPermissionsChange: CheckboxChangeHandlerType;
onMessageAudioChange: CheckboxChangeHandlerType;
onMinimizeToAndStartInSystemTrayChange: CheckboxChangeHandlerType;
onMinimizeToSystemTrayChange: CheckboxChangeHandlerType;
onNotificationAttentionChange: CheckboxChangeHandlerType;
@ -252,6 +253,7 @@ export function Preferences({
hasLinkPreviews,
hasMediaCameraPermissions,
hasMediaPermissions,
hasMessageAudio,
hasMinimizeToAndStartInSystemTray,
hasMinimizeToSystemTray,
hasNotificationAttention,
@ -264,7 +266,6 @@ export function Preferences({
hasTypingIndicators,
i18n,
initialSpellCheckSetting,
isAudioNotificationsSupported,
isAutoDownloadUpdatesSupported,
isAutoLaunchSupported,
isFormattingFlagEnabled,
@ -290,6 +291,7 @@ export function Preferences({
onLastSyncTimeChange,
onMediaCameraPermissionsChange,
onMediaPermissionsChange,
onMessageAudioChange,
onMinimizeToAndStartInSystemTrayChange,
onMinimizeToSystemTrayChange,
onNotificationAttentionChange,
@ -857,15 +859,6 @@ export function Preferences({
onChange={onNotificationAttentionChange}
/>
)}
{isAudioNotificationsSupported && (
<Checkbox
checked={hasAudioNotifications}
label={i18n('icu:audioNotificationDescription')}
moduleClassName="Preferences__checkbox"
name="audioNotification"
onChange={onAudioNotificationsChange}
/>
)}
<Checkbox
checked={hasCountMutedConversations}
label={i18n('icu:countMutedConversationsDescription')}
@ -901,6 +894,24 @@ export function Preferences({
}
/>
</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) {

View file

@ -81,6 +81,7 @@ export class SettingsChannel extends EventEmitter {
this.installSetting('notificationSetting');
this.installSetting('notificationDrawAttention');
this.installSetting('audioMessage');
this.installSetting('audioNotification');
this.installSetting('countMutedConversations');

View file

@ -4,32 +4,30 @@
import os from 'os';
import { debounce } from 'lodash';
import EventEmitter from 'events';
import { Sound } from '../util/Sound';
import {
AudioNotificationSupport,
getAudioNotificationSupport,
shouldHideExpiringMessageBody,
} from '../types/Settings';
import { Sound, SoundType } from '../util/Sound';
import { shouldHideExpiringMessageBody } from '../types/Settings';
import OS from '../util/os/osMain';
import * as log from '../logging/log';
import { makeEnumParser } from '../util/enum';
import { missingCaseError } from '../util/missingCaseError';
import type { StorageInterface } from '../types/Storage.d';
import type { LocalizerType } from '../types/Util';
import { drop } from '../util/drop';
type NotificationDataType = Readonly<{
conversationId: string;
storyId?: string;
isExpiringMessage: boolean;
messageId: string;
senderTitle: string;
message: string;
notificationIconUrl?: undefined | string;
isExpiringMessage: boolean;
reaction?: {
emoji: string;
targetAuthorUuid: string;
targetTimestamp: number;
};
senderTitle: string;
storyId?: string;
useTriToneSound?: boolean;
wasShown?: boolean;
}>;
@ -133,6 +131,7 @@ class NotificationService extends EventEmitter {
onNotificationClick,
silent,
title,
useTriToneSound,
}: Readonly<{
icon?: string;
message: string;
@ -140,28 +139,25 @@ class NotificationService extends EventEmitter {
onNotificationClick: () => void;
silent: boolean;
title: string;
useTriToneSound?: boolean;
}>): void {
log.info('NotificationService: showing a notification');
this.lastNotification?.close();
const audioNotificationSupport = getAudioNotificationSupport(OS);
const notification = new window.Notification(title, {
body: OS.isLinux() ? filterNotificationText(message) : message,
icon,
silent:
silent || audioNotificationSupport !== AudioNotificationSupport.Native,
silent: true,
tag: messageId,
});
notification.onclick = onNotificationClick;
if (
!silent &&
audioNotificationSupport === AudioNotificationSupport.Custom
) {
if (!silent) {
const soundType =
messageId && !useTriToneSound ? SoundType.Pop : SoundType.TriTone;
// 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;
@ -273,12 +269,13 @@ class NotificationService extends EventEmitter {
const {
conversationId,
storyId,
messageId,
senderTitle,
message,
isExpiringMessage,
message,
messageId,
reaction,
senderTitle,
storyId,
useTriToneSound,
wasShown,
} = notificationData;
@ -346,13 +343,15 @@ class NotificationService extends EventEmitter {
};
this.notify({
title: notificationTitle,
icon: notificationIconUrl,
messageId,
message: notificationMessage,
silent: !shouldPlayNotificationSound,
onNotificationClick: () => {
this.emit('click', conversationId, messageId, storyId);
},
silent: !shouldPlayNotificationSound,
title: notificationTitle,
useTriToneSound,
});
}

View file

@ -89,6 +89,7 @@ import { strictAssert } from '../../util/assert';
import { makeQuote } from '../../util/makeQuote';
import { sendEditedMessage as doSendEditedMessage } from '../../util/sendEditedMessage';
import { maybeBlockSendForFormattingModal } from '../../util/maybeBlockSendForFormattingModal';
import { Sound, SoundType } from '../../util/Sound';
// State
// eslint-disable-next-line local-rules/type-alias-readonlydeep
@ -616,6 +617,8 @@ function sendMultiMediaMessage(
);
dispatch(incrementSendCounter(conversationId));
dispatch(setComposerDisabledState(conversationId, false));
drop(new Sound({ soundType: SoundType.Whoosh }).play());
},
}
);

View file

@ -89,7 +89,8 @@ async function notifyForCall(
onNotificationClick: () => {
window.IPC.showWindow();
},
silent: false,
// The ringtone plays so we don't need sound for the notification
silent: true,
});
}

View file

@ -13,7 +13,7 @@ import {
import { globalMessageAudio } from '../../services/globalMessageAudio';
import { strictAssert } from '../../util/assert';
import * as log from '../../logging/log';
import { Sound } from '../../util/Sound';
import { Sound, SoundType } from '../../util/Sound';
import { getConversations } from '../selectors/conversations';
import { SeenStatus } from '../../MessageSeenStatus';
import { markViewed } from '../ducks/conversations';
@ -21,10 +21,10 @@ import * as Errors from '../../types/errors';
import { usePrevious } from '../../hooks/usePrevious';
const stateChangeConfirmUpSound = new Sound({
src: 'sounds/state-change_confirm-up.ogg',
soundType: SoundType.VoiceNoteStart,
});
const stateChangeConfirmDownSound = new Sound({
src: 'sounds/state-change_confirm-down.ogg',
soundType: SoundType.VoiceNoteEnd,
});
/**

View file

@ -19,74 +19,6 @@ describe('Settings', () => {
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', () => {
it('returns true on macOS', () => {
sandbox.stub(process, 'platform').value('darwin');

View file

@ -8,27 +8,6 @@ import { isProduction } from '../util/version';
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:
// https://github.com/electron/electron/issues/11189
export const isNotificationGroupingSupported = (OS: OSType): boolean =>

View file

@ -62,6 +62,7 @@ export type StorageAccessType = {
'spell-check': boolean;
'system-tray-setting': SystemTraySetting;
'theme-setting': ThemeSettingType;
audioMessage: boolean;
attachmentMigration_isComplete: boolean;
attachmentMigration_lastProcessedIndex: number;
blocked: ReadonlyArray<string>;

View file

@ -2,14 +2,26 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import { missingCaseError } from './missingCaseError';
export enum SoundType {
CallingHangUp,
CallingPresenting,
Pop,
Ringtone,
TriTone,
VoiceNoteEnd,
VoiceNoteStart,
Whoosh,
}
export type SoundOpts = {
loop?: boolean;
src: string;
soundType: SoundType;
};
export class Sound {
static sounds = new Map();
static sounds = new Map<SoundType, AudioBuffer>();
private static context: AudioContext | undefined;
@ -17,27 +29,29 @@ export class Sound {
private node?: AudioBufferSourceNode;
private readonly src: string;
private readonly soundType: SoundType;
constructor(options: SoundOpts) {
this.loop = Boolean(options.loop);
this.src = options.src;
this.soundType = options.soundType;
}
async play(): Promise<void> {
if (!Sound.sounds.has(this.src)) {
let soundBuffer = Sound.sounds.get(this.soundType);
if (!soundBuffer) {
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);
Sound.sounds.set(this.src, decodedBuffer);
Sound.sounds.set(this.soundType, decodedBuffer);
soundBuffer = decodedBuffer;
} catch (err) {
log.error(`Sound error: ${err}`);
return;
}
}
const soundBuffer = Sound.sounds.get(this.src);
const soundNode = this.context.createBufferSource();
soundNode.buffer = soundBuffer;
@ -87,4 +101,40 @@ export class Sound {
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);
}
}

View file

@ -3,7 +3,7 @@
import PQueue from 'p-queue';
import { MINUTE } from './durations';
import { Sound } from './Sound';
import { Sound, SoundType } from './Sound';
const ringtoneEventQueue = new PQueue({
concurrency: 1,
@ -21,7 +21,7 @@ class CallingTones {
}
const tone = new Sound({
src: 'sounds/navigation-cancel.ogg',
soundType: SoundType.CallingHangUp,
});
await tone.play();
}
@ -40,7 +40,7 @@ class CallingTones {
this.ringtone = new Sound({
loop: true,
src: 'sounds/ringtone_minimal.ogg',
soundType: SoundType.Ringtone,
});
await this.ringtone.play();
@ -63,7 +63,7 @@ class CallingTones {
}
const tone = new Sound({
src: 'sounds/navigation_selection-complete-celebration.ogg',
soundType: SoundType.CallingPresenting,
});
await tone.play();

View file

@ -50,6 +50,7 @@ type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
export type IPCEventsValuesType = {
alwaysRelayCalls: boolean | undefined;
audioNotification: boolean | undefined;
audioMessage: boolean;
autoDownloadUpdate: boolean;
autoLaunch: boolean;
callRingtoneNotification: boolean;
@ -371,6 +372,8 @@ export function createIPCEvents(
window.storage.get('notification-draw-attention', false),
setNotificationDrawAttention: 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'),
setAudioNotification: value =>
window.storage.put('audio-notification', value),

View file

@ -38,6 +38,7 @@ installCallback('shouldShowStoriesSettings');
installCallback('syncRequest');
installSetting('alwaysRelayCalls');
installSetting('audioMessage');
installSetting('audioNotification');
installSetting('autoDownloadUpdate');
installSetting('autoLaunch');

View file

@ -44,6 +44,7 @@ SettingsWindowProps.onRender(
hasLinkPreviews,
hasMediaCameraPermissions,
hasMediaPermissions,
hasMessageAudio,
hasMinimizeToAndStartInSystemTray,
hasMinimizeToSystemTray,
hasNotificationAttention,
@ -55,7 +56,6 @@ SettingsWindowProps.onRender(
hasTextFormatting,
hasTypingIndicators,
initialSpellCheckSetting,
isAudioNotificationsSupported,
isAutoDownloadUpdatesSupported,
isAutoLaunchSupported,
isFormattingFlagEnabled,
@ -80,6 +80,7 @@ SettingsWindowProps.onRender(
onLastSyncTimeChange,
onMediaCameraPermissionsChange,
onMediaPermissionsChange,
onMessageAudioChange,
onMinimizeToAndStartInSystemTrayChange,
onMinimizeToSystemTrayChange,
onNotificationAttentionChange,
@ -141,6 +142,7 @@ SettingsWindowProps.onRender(
hasLinkPreviews={hasLinkPreviews}
hasMediaCameraPermissions={hasMediaCameraPermissions}
hasMediaPermissions={hasMediaPermissions}
hasMessageAudio={hasMessageAudio}
hasMinimizeToAndStartInSystemTray={hasMinimizeToAndStartInSystemTray}
hasMinimizeToSystemTray={hasMinimizeToSystemTray}
hasNotificationAttention={hasNotificationAttention}
@ -153,7 +155,6 @@ SettingsWindowProps.onRender(
hasTypingIndicators={hasTypingIndicators}
i18n={i18n}
initialSpellCheckSetting={initialSpellCheckSetting}
isAudioNotificationsSupported={isAudioNotificationsSupported}
isAutoDownloadUpdatesSupported={isAutoDownloadUpdatesSupported}
isAutoLaunchSupported={isAutoLaunchSupported}
isFormattingFlagEnabled={isFormattingFlagEnabled}
@ -180,6 +181,7 @@ SettingsWindowProps.onRender(
onLastSyncTimeChange={onLastSyncTimeChange}
onMediaCameraPermissionsChange={onMediaCameraPermissionsChange}
onMediaPermissionsChange={onMediaPermissionsChange}
onMessageAudioChange={onMessageAudioChange}
onMinimizeToAndStartInSystemTrayChange={
onMinimizeToAndStartInSystemTrayChange
}

View file

@ -20,6 +20,7 @@ function doneRendering() {
ipcRenderer.send('settings-done-rendering');
}
const settingMessageAudio = createSetting('audioMessage');
const settingAudioNotification = createSetting('audioNotification');
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
const settingAutoLaunch = createSetting('autoLaunch');
@ -152,6 +153,7 @@ async function renderPreferences() {
hasLinkPreviews,
hasMediaCameraPermissions,
hasMediaPermissions,
hasMessageAudio,
hasNotificationAttention,
hasReadReceipts,
hasRelayCalls,
@ -193,6 +195,7 @@ async function renderPreferences() {
hasLinkPreviews: settingLinkPreview.getValue(),
hasMediaCameraPermissions: settingMediaCameraPermissions.getValue(),
hasMediaPermissions: settingMediaPermissions.getValue(),
hasMessageAudio: settingMessageAudio.getValue(),
hasNotificationAttention: settingNotificationDrawAttention.getValue(),
hasReadReceipts: settingReadReceipts.getValue(),
hasRelayCalls: settingRelayCalls.getValue(),
@ -253,6 +256,7 @@ async function renderPreferences() {
hasLinkPreviews,
hasMediaCameraPermissions,
hasMediaPermissions,
hasMessageAudio,
hasMinimizeToAndStartInSystemTray,
hasMinimizeToSystemTray,
hasNotificationAttention,
@ -293,7 +297,6 @@ async function renderPreferences() {
shouldShowStoriesSettings,
// Limited support features
isAudioNotificationsSupported: Settings.isAudioNotificationSupported(OS),
isAutoDownloadUpdatesSupported: Settings.isAutoDownloadUpdatesSupported(OS),
isAutoLaunchSupported: Settings.isAutoLaunchSupported(OS),
isHideMenuBarSupported: Settings.isHideMenuBarSupported(OS),
@ -347,6 +350,7 @@ async function renderPreferences() {
onMediaCameraPermissionsChange: attachRenderCallback(
settingMediaCameraPermissions.setValue
),
onMessageAudioChange: attachRenderCallback(settingMessageAudio.setValue),
onMinimizeToAndStartInSystemTrayChange: attachRenderCallback(
async (value: boolean) => {
await settingSystemTraySetting.setValue(