[MM-57486] [MM-58008] Calls: Mobile ringing for incoming calls (#7984)

* notification ringing, settings screen, native code patch, ringing mp3s

* i18n

* play preview on first press

* prevent playing from background (only affects Android) to match iOS beh

* stop ringing/vibration on entering background

* ring when coming back from background and new incoming call is present

* no push notification sound when it's a call; improve ringing

* move sounds to asset folder; copy on postinstall for android bundling

* make Ringtone type a string enum

* make Android ring async + await ring and stop; changes from PR comments

* missing fields after merge

* release lock on an exception

* cancel sample ringing when turning notifications off

* copy sound files for android build

* typo

* update snapshots

* testing if the problem is copying the mp3 files

* fix android mp3 assets when building for non-release

* add sounds to .gitignore

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Christopher Poile 2024-07-03 10:22:46 -04:00 committed by GitHub
parent 331d3130bc
commit 92bdb2847b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 884 additions and 24 deletions

5
.gitignore vendored
View file

@ -117,4 +117,7 @@ launch.json
.metro-health-check*
libraries/**/**/build
libraries/**/**/.build
libraries/**/**/.build
# Android sounds
android/app/src/main/res/raw/*

View file

@ -269,6 +269,10 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
"locale": "",
"nickname": "",
"notify_props": {
"calls_desktop_sound": "true",
"calls_mobile_notification_sound": "",
"calls_mobile_sound": "",
"calls_notification_sound": "Calm",
"channel": "true",
"comments": "never",
"desktop": "mention",
@ -584,6 +588,10 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
"locale": "",
"nickname": "",
"notify_props": {
"calls_desktop_sound": "true",
"calls_mobile_notification_sound": "",
"calls_mobile_sound": "",
"calls_notification_sound": "Calm",
"channel": "true",
"comments": "never",
"desktop": "mention",
@ -899,6 +907,10 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
"locale": "",
"nickname": "",
"notify_props": {
"calls_desktop_sound": "true",
"calls_mobile_notification_sound": "",
"calls_mobile_sound": "",
"calls_notification_sound": "Calm",
"channel": "true",
"comments": "never",
"desktop": "mention",
@ -933,6 +945,10 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
"locale": "",
"nickname": "",
"notify_props": {
"calls_desktop_sound": "true",
"calls_mobile_notification_sound": "",
"calls_mobile_sound": "",
"calls_notification_sound": "Calm",
"channel": "true",
"comments": "never",
"desktop": "mention",

View file

@ -4,6 +4,7 @@
import React from 'react';
import {Image} from 'react-native';
import {Ringtone} from '@constants/calls';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
@ -51,6 +52,10 @@ describe('components/channel_list_row', () => {
highlight_keys: '',
push: 'mention',
push_status: 'away',
calls_desktop_sound: 'true',
calls_mobile_sound: '',
calls_notification_sound: Ringtone.Calm,
calls_mobile_notification_sound: '',
},
};
@ -80,6 +85,10 @@ describe('components/channel_list_row', () => {
highlight_keys: '',
push: 'mention',
push_status: 'away',
calls_desktop_sound: 'true',
calls_mobile_sound: '',
calls_notification_sound: Ringtone.Calm,
calls_mobile_notification_sound: '',
},
};

View file

@ -32,6 +32,19 @@ const REACTION_TIMEOUT = 10000;
const REACTION_LIMIT = 20;
const CALL_QUALITY_RESET_MS = toMilliseconds({minutes: 1});
const CAPTION_TIMEOUT = 5000;
const RING_LENGTH = 30000;
export enum Ringtone {
Calm = 'Calm',
Dynamic = 'Dynamic',
Urgent = 'Urgent',
Cheerful = 'Cheerful',
}
const RINGTONE_DEFAULT = Ringtone.Calm;
// 30 seconds of vibration (there is no loop setting)
const RINGTONE_VIBRATE_PATTERN = [1000, 500, 1000, 500, 1000, 500, 1000, 500, 1000, 1000, 500, 1000, 500, 1000, 500, 1000, 500, 1000, 1000, 500, 1000, 500, 1000, 500, 1000, 500, 1000, 1000, 500, 1000, 500, 1000, 500, 1000, 500, 1000, 1000, 500, 1000, 500, 1000];
export enum MessageBarType {
Microphone,
@ -57,4 +70,7 @@ export default {
JOB_TYPE_RECORDING,
JOB_TYPE_TRANSCRIBING,
JOB_TYPE_CAPTIONING,
RING_LENGTH,
RINGTONE_DEFAULT,
RINGTONE_VIBRATE_PATTERN,
};

View file

@ -64,6 +64,7 @@ export const SETTINGS_NOTIFICATION_AUTO_RESPONDER = 'SettingsNotificationAutoRes
export const SETTINGS_NOTIFICATION_EMAIL = 'SettingsNotificationEmail';
export const SETTINGS_NOTIFICATION_MENTION = 'SettingsNotificationMention';
export const SETTINGS_NOTIFICATION_PUSH = 'SettingsNotificationPush';
export const SETTINGS_NOTIFICATION_CALL = 'SettingsNotificationCall';
export const SHARE_FEEDBACK = 'ShareFeedback';
export const SNACK_BAR = 'SnackBar';
export const SSO = 'SSO';
@ -139,6 +140,7 @@ export default {
SETTINGS_NOTIFICATION_EMAIL,
SETTINGS_NOTIFICATION_MENTION,
SETTINGS_NOTIFICATION_PUSH,
SETTINGS_NOTIFICATION_CALL,
SHARE_FEEDBACK,
SNACK_BAR,
SSO,

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Ringtone} from '@constants/calls';
import DatabaseManager from '@database/manager';
import {buildPreferenceKey} from '@database/operator/server_data_operator/comparators';
import {shouldUpdateUserRecord} from '@database/operator/server_data_operator/comparators/user';
@ -82,6 +83,10 @@ describe('*** Operator: User Handlers tests ***', () => {
comments: 'never',
desktop_notification_sound: 'Hello',
push_status: 'online',
calls_desktop_sound: 'true',
calls_mobile_sound: '',
calls_notification_sound: Ringtone.Calm,
calls_mobile_notification_sound: '',
},
last_picture_update: 1604686302260,
locale: 'en',

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Ringtone} from '@constants/calls';
import {OperationType} from '@constants/database';
import {transformPreferenceRecord, transformUserRecord} from '@database/operator/server_data_operator/transformers/user';
import {createTestConnection} from '@database/operator/utils/create_test_connection';
@ -66,6 +67,10 @@ describe('*** USER Prepare Records Test ***', () => {
comments: 'never',
desktop_notification_sound: 'Hello',
push_status: 'online',
calls_desktop_sound: 'true',
calls_mobile_sound: '',
calls_notification_sound: Ringtone.Calm,
calls_mobile_notification_sound: '',
},
last_picture_update: 1604686302260,
locale: 'en',

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {CallsManager} from '@calls/calls_manager';
import DatabaseManager from '@database/manager';
import {getAllServerCredentials} from '@init/credentials';
import {initialLaunch} from '@init/launch';
@ -47,6 +48,7 @@ export async function initialize() {
GlobalEventHandler.init();
ManagedApp.init();
SessionManager.init();
CallsManager.initialize();
}
}

View file

@ -255,7 +255,10 @@ class PushNotifications {
this.processNotification(notification);
}
completion({alert: false, sound: true, badge: true});
// Always play a sound, except when this is a foreground notification about a call
const sound = !(notification.foreground && isCallsStartedMessage(notification.payload));
completion({alert: false, sound, badge: true});
};
onRemoteNotificationsRegistered = async (event: Registered) => {

View file

@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {AppState, Platform} from 'react-native';
import {callsOnAppStateChange} from '@calls/state';
const initialize = () => {
if (Platform.OS === 'android') {
AppState.addEventListener('blur', () => {
callsOnAppStateChange('inactive');
});
AppState.addEventListener('focus', () => {
callsOnAppStateChange('active');
});
} else {
AppState.addEventListener('change', callsOnAppStateChange);
}
};
export const CallsManager = {
initialize,
};

View file

@ -8,7 +8,7 @@ import {Pressable, Text, View} from 'react-native';
import {switchToChannelById} from '@actions/remote/channel';
import {fetchProfilesInChannel} from '@actions/remote/user';
import {dismissIncomingCall} from '@calls/actions/calls';
import {removeIncomingCall} from '@calls/state';
import {playIncomingCallsRinging, removeIncomingCall} from '@calls/state';
import {ChannelType, type IncomingCallNotification} from '@calls/types/calls';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
@ -17,6 +17,7 @@ import {CALL_NOTIFICATION_BAR_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import {useAppState} from '@hooks/device';
import WebsocketManager from '@managers/websocket_manager';
import {getServerDisplayName} from '@queries/app/servers';
import ChannelMembershipModel from '@typings/database/models/servers/channel_membership';
@ -107,7 +108,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
type Props = {
servers: ServersModel[];
incomingCall: IncomingCallNotification;
currentUserId: string;
currentUserId?: string;
userStatus?: string;
teammateNameDisplay: string;
members?: ChannelMembershipModel[];
onCallsScreen?: boolean;
@ -117,12 +119,14 @@ export const CallNotification = ({
servers,
incomingCall,
currentUserId,
userStatus,
teammateNameDisplay,
members,
onCallsScreen,
}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const appState = useAppState();
const theme = useTheme();
const style = getStyleSheet(theme);
const [serverName, setServerName] = useState('');
@ -135,6 +139,10 @@ export const CallNotification = ({
}
}, []);
useEffect(() => {
playIncomingCallsRinging(incomingCall.serverUrl, incomingCall.callID, userStatus || '');
}, [incomingCall.serverUrl, incomingCall.callID, appState]);
// We only need to getServerDisplayName once
useEffect(() => {
async function getName() {

View file

@ -9,8 +9,7 @@ import {observeAllActiveServers} from '@app/queries/app/servers';
import {CallNotification} from '@calls/components/call_notification/call_notification';
import DatabaseManager from '@database/manager';
import {observeChannelMembers} from '@queries/servers/channel';
import {observeCurrentUserId} from '@queries/servers/system';
import {observeTeammateNameDisplay} from '@queries/servers/user';
import {observeCurrentUser, observeTeammateNameDisplay} from '@queries/servers/user';
import type {IncomingCallNotification} from '@calls/types/calls';
@ -20,8 +19,15 @@ type OwnProps = {
const enhanced = withObservables(['incomingCall'], ({incomingCall}: OwnProps) => {
const database = of$(DatabaseManager.serverDatabases[incomingCall.serverUrl]?.database);
const currentUserId = database.pipe(
switchMap((db) => (db ? observeCurrentUserId(db) : of$(''))),
const currentUser = database.pipe(
switchMap((db) => (db ? observeCurrentUser(db) : of$(null))),
);
const currentUserId = currentUser.pipe(
switchMap((u) => of$(u?.id)),
distinctUntilChanged(),
);
const userStatus = currentUser.pipe(
switchMap((u) => of$(u?.status)),
distinctUntilChanged(),
);
const teammateNameDisplay = database.pipe(
@ -36,6 +42,7 @@ const enhanced = withObservables(['incomingCall'], ({incomingCall}: OwnProps) =>
return {
servers: observeAllActiveServers(),
currentUserId,
userStatus,
teammateNameDisplay,
members,
};

View file

@ -60,12 +60,14 @@ import {
DefaultGlobalCallsState,
DefaultIncomingCalls,
type GlobalCallsState,
type IncomingCalls,
} from '@calls/types/calls';
import {License} from '@constants';
import Calls from '@constants/calls';
import DatabaseManager from '@database/manager';
import type {CallJobState, LiveCaptionData} from '@mattermost/calls/lib/types';
import type UserModel from '@typings/database/models/servers/user';
jest.mock('@calls/alerts');
@ -1339,11 +1341,12 @@ describe('useCallsState', () => {
};
const initialCurrentCallState: CurrentCall | null = null;
const initialIncomingCalls = DefaultIncomingCalls;
const expectedIncomingCalls = {
const expectedIncomingCalls: IncomingCalls = {
...DefaultIncomingCalls,
incomingCalls: [{
callID: 'callDM',
callerID: 'user-5',
callerModel: {username: 'user-5'},
callerModel: {username: 'user-5'} as UserModel,
channelID: 'channel-private',
myUserId: 'myId',
serverUrl: 'server1',

View file

@ -2,6 +2,8 @@
// See LICENSE.txt for license information.
import {mosThreshold} from '@mattermost/calls/lib/rtc_monitor';
import {AppState, type AppStateStatus} from 'react-native';
import InCallManager from 'react-native-incall-manager';
import {Navigation} from 'react-native-navigation';
import {updateThreadFollowing} from '@actions/remote/thread';
@ -37,10 +39,11 @@ import {Calls, General, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {getChannelById} from '@queries/servers/channel';
import {getThreadById} from '@queries/servers/thread';
import {getUserById} from '@queries/servers/user';
import {getCurrentUser, getUserById} from '@queries/servers/user';
import {isDMorGM} from '@utils/channel';
import {generateId} from '@utils/general';
import {logDebug} from '@utils/log';
import {isMainActivity} from '@utils/helpers';
import {logDebug, logError} from '@utils/log';
import type {CallJobState, LiveCaptionData, UserReactionData} from '@mattermost/calls/lib/types';
@ -144,7 +147,8 @@ export const processIncomingCalls = async (serverUrl: string, calls: Call[], kee
}
newIncoming.sort((a, b) => a.startAt - b.startAt);
setIncomingCalls({incomingCalls: newIncoming});
setIncomingCalls({...getIncomingCalls(), incomingCalls: newIncoming});
};
const getChannelIdFromCallId = (serverUrl: string, callId: string) => {
@ -162,10 +166,12 @@ export const removeIncomingCall = (serverUrl: string, callId: string, channelId?
return;
}
stopIncomingCallsRinging();
const incomingCalls = getIncomingCalls();
const newIncomingCalls = incomingCalls.incomingCalls.filter((ic) => ic.callID !== callId);
if (incomingCalls.incomingCalls.length !== newIncomingCalls.length) {
setIncomingCalls({incomingCalls: newIncomingCalls});
setIncomingCalls({...incomingCalls, incomingCalls: newIncomingCalls});
}
let chId = channelId;
@ -184,6 +190,102 @@ export const removeIncomingCall = (serverUrl: string, callId: string, channelId?
setCallsState(serverUrl, {...callsState, calls: nextCalls});
};
let previousAppState: AppStateStatus;
export const callsOnAppStateChange = async (appState: AppStateStatus) => {
if (appState === previousAppState || !isMainActivity()) {
return;
}
previousAppState = appState;
switch (appState) {
case 'inactive':
case 'background':
InCallManager.stopRingtone();
setIncomingCalls({...getIncomingCalls(), currentRingingCallId: undefined});
break;
}
};
const getRingtoneOrNone = async (serverUrl: string) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const user = await getCurrentUser(database);
if (!user) {
// This shouldn't happen, so don't bother localizing and displaying an alert.
return 'none';
}
const enabled = user.notifyProps?.calls_mobile_sound ? user.notifyProps.calls_mobile_sound === 'true' : user.notifyProps?.calls_desktop_sound === 'true';
if (!enabled) {
return 'none';
}
let tone = user.notifyProps?.calls_mobile_notification_sound ? user.notifyProps.calls_mobile_notification_sound : user.notifyProps?.calls_notification_sound;
if (!tone) {
tone = Calls.RINGTONE_DEFAULT;
}
return 'calls_' + tone.toLowerCase();
} catch (error) {
logError('failed to getServerDatabase in getRingtoneOrNone', error);
return 'none';
}
};
const shouldRing = (callId: string, userStatus: string) => {
// Do not ring if we are in the background
if (AppState.currentState !== 'active' || userStatus === General.DND || userStatus === General.OUT_OF_OFFICE) {
return false;
}
// Do not ring if we are already ringing, or we have no incoming calls, or we have rung for this call already
const incomingCalls = getIncomingCalls();
if (incomingCalls.currentRingingCallId || incomingCalls.incomingCalls.length === 0 || incomingCalls.callIdHasRung[callId]) {
return false;
}
// Do not ring if we are in a call
const currentCall = getCurrentCall();
return !currentCall;
};
export const playIncomingCallsRinging = async (serverUrl: string, callId: string, userStatus: string) => {
if (!shouldRing(callId, userStatus)) {
return;
}
const ringTone = await getRingtoneOrNone(serverUrl);
if (ringTone === 'none') {
return;
}
const incomingCalls = getIncomingCalls();
setIncomingCalls({
...incomingCalls,
currentRingingCallId: callId,
callIdHasRung: {...incomingCalls.callIdHasRung, [callId]: true},
});
InCallManager.startRingtone(ringTone, Calls.RINGTONE_VIBRATE_PATTERN);
setTimeout(() => {
const incoming = getIncomingCalls();
if (incoming.currentRingingCallId === callId) {
InCallManager.stopRingtone();
setIncomingCalls({...getIncomingCalls(), currentRingingCallId: undefined});
}
}, Calls.RING_LENGTH);
};
const stopIncomingCallsRinging = () => {
const incomingCalls = getIncomingCalls();
if (!incomingCalls.currentRingingCallId) {
return;
}
InCallManager.stopRingtone();
setIncomingCalls({...incomingCalls, currentRingingCallId: undefined});
};
export const setCallForChannel = (serverUrl: string, channelId: string, call?: Call, enabled?: boolean) => {
const callsState = getCallsState(serverUrl);
let nextEnabled = callsState.enabled;

View file

@ -49,10 +49,13 @@ export type IncomingCallNotification = {
export type IncomingCalls = {
incomingCalls: IncomingCallNotification[];
currentRingingCallId?: string;
callIdHasRung: Dictionary<boolean>;
}
export const DefaultIncomingCalls: IncomingCalls = {
incomingCalls: [],
callIdHasRung: {},
};
export type Call = {

View file

@ -226,6 +226,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
case Screens.SETTINGS_NOTIFICATION_PUSH:
screen = withServerDatabase(require('@screens/settings/notification_push').default);
break;
case Screens.SETTINGS_NOTIFICATION_CALL:
screen = withServerDatabase(require('@screens/settings/notification_call').default);
break;
case Screens.SHARE_FEEDBACK:
screen = withServerDatabase(require('@screens/share_feedback').default);
break;

View file

@ -4,6 +4,7 @@
import React from 'react';
import {Preferences, View as ViewConstants} from '@constants';
import {Ringtone} from '@constants/calls';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
@ -62,6 +63,10 @@ describe('components/integration_selector/selected_option', () => {
highlight_keys: '',
push: 'mention',
push_status: 'ooo',
calls_desktop_sound: 'true',
calls_mobile_sound: '',
calls_notification_sound: Ringtone.Calm,
calls_mobile_notification_sound: '',
},
email: 'johndoe@me.com',
auth_service: 'dummy',

View file

@ -4,6 +4,7 @@
import React from 'react';
import {Preferences, View as ViewConstants} from '@constants';
import {Ringtone} from '@constants/calls';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';
@ -42,6 +43,10 @@ describe('components/integration_selector/selected_options', () => {
highlight_keys: '',
push: 'mention',
push_status: 'ooo',
calls_desktop_sound: 'true',
calls_mobile_sound: '',
calls_notification_sound: Ringtone.Calm,
calls_mobile_notification_sound: '',
},
email: 'johndoe@me.com',
auth_service: 'dummy',

View file

@ -86,6 +86,12 @@ export const NotificationsOptionConfig: Record<string, SettingConfigDetails> = {
icon: 'cellphone',
testID: 'notification_settings.push_notification',
},
call_notification: {
defaultMessage: 'Call Notifications',
i18nId: t('notification_settings.calls'),
icon: 'phone-in-talk',
testID: 'notification_settings.call_notification',
},
email: {
defaultMessage: 'Email',
i18nId: t('notification_settings.email'),

View file

@ -0,0 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {observeCurrentUser} from '@queries/servers/user';
import NotificationCall from '@screens/settings/notification_call/notification_call';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
return {
currentUser: observeCurrentUser(database),
};
});
export default withDatabase(enhanced(NotificationCall));

View file

@ -0,0 +1,192 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo, useState} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import InCallManager from 'react-native-incall-manager';
import {updateMe} from '@actions/remote/user';
import SettingBlock from '@components/settings/block';
import SettingContainer from '@components/settings/container';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {Calls} from '@constants';
import {Ringtone} from '@constants/calls';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useBackNavigation from '@hooks/navigate_back';
import {popTopScreen} from '@screens/navigation';
import {changeOpacity} from '@utils/theme';
import {getNotificationProps} from '@utils/user';
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
componentId: AvailableScreens;
currentUser?: UserModel;
};
const {footerText} = defineMessages({
footerText: {
id: 'notification_settings.calls.callsInfo',
defaultMessage: 'Note: silent mode must be off to hear the ringtone preview.',
},
});
const NotificationCall = ({componentId, currentUser}: Props) => {
const serverUrl = useServerUrl();
const intl = useIntl();
const theme = useTheme();
const notifyProps = useMemo(() => getNotificationProps(currentUser), [currentUser?.notifyProps]);
const initialCallsMobileSound = useMemo(() => Boolean(notifyProps?.calls_mobile_sound ? notifyProps.calls_mobile_sound === 'true' : notifyProps?.calls_desktop_sound === 'true'),
[/* dependency array should remain empty */]);
const [callsMobileSound, setCallsMobileSound] = useState(initialCallsMobileSound);
const initialCallsMobileNotificationSound = useMemo(() => {
let initialSound = notifyProps?.calls_mobile_notification_sound ? notifyProps.calls_mobile_notification_sound : notifyProps?.calls_notification_sound;
if (!initialSound) {
initialSound = Calls.RINGTONE_DEFAULT;
}
return initialSound;
}, [/* dependency array should remain empty */]);
const [callsMobileNotificationSound, setCallsMobileNotificationSound] = useState(initialCallsMobileNotificationSound);
const [playingRingtone, setPlayingRingtone] = useState(false);
const close = useCallback(() => {
InCallManager.stopRingtone();
popTopScreen(componentId);
}, [componentId]);
const selectOption = useCallback(async (value: string) => {
const tone = 'calls_' + value.toLowerCase();
if (value !== callsMobileNotificationSound) {
setCallsMobileNotificationSound(value);
await InCallManager.stopRingtone();
await InCallManager.startRingtone(tone, Calls.RINGTONE_VIBRATE_PATTERN);
setPlayingRingtone(true);
return;
}
if (playingRingtone) {
await InCallManager.stopRingtone();
setPlayingRingtone(false);
} else {
await InCallManager.startRingtone(tone, Calls.RINGTONE_VIBRATE_PATTERN);
setPlayingRingtone(true);
}
}, [callsMobileNotificationSound, playingRingtone]);
const selectNotificationOnOff = useCallback(async (on: boolean) => {
setCallsMobileSound(on);
if (!on) {
await InCallManager.stopRingtone();
}
}, []);
const canSaveSettings = useCallback(() => {
const cmsString = callsMobileSound ? 'true' : 'false';
const cms = cmsString !== notifyProps.calls_mobile_sound;
const cmns = callsMobileNotificationSound !== notifyProps.calls_mobile_notification_sound;
return cms || cmns;
}, [notifyProps, callsMobileSound, callsMobileNotificationSound]);
const saveNotificationSettings = useCallback(() => {
const canSave = canSaveSettings();
if (canSave) {
const cmsString = callsMobileSound ? 'true' : 'false';
const notify_props: UserNotifyProps = {
...notifyProps,
calls_mobile_sound: cmsString,
calls_mobile_notification_sound: callsMobileNotificationSound,
};
updateMe(serverUrl, {notify_props});
}
close();
}, [serverUrl, canSaveSettings, close, notifyProps, callsMobileSound, callsMobileNotificationSound, playingRingtone]);
useBackNavigation(saveNotificationSettings);
useAndroidHardwareBackHandler(componentId, saveNotificationSettings);
return (
<SettingContainer testID='call_notification_settings'>
<SettingOption
label={intl.formatMessage({
id: 'notification_settings.calls.enable_sound',
defaultMessage: 'Notification sound for incoming calls',
})}
action={selectNotificationOnOff}
testID='notification_settings.calls.enable_sound.option'
type='toggle'
selected={callsMobileSound}
/>
{callsMobileSound && (
<SettingBlock footerText={footerText}>
<SettingOption
action={selectOption}
label={intl.formatMessage({
id: 'notification_settings.calls.dynamic',
defaultMessage: 'Dynamic',
})}
selected={callsMobileNotificationSound === Ringtone.Dynamic}
testID='notification_settings.calls.dynamic.option'
type='select'
value={Ringtone.Dynamic}
icon='volume-high'
iconColor={changeOpacity(theme.centerChannelColor, 0.56)}
/>
<SettingSeparator/>
<SettingOption
action={selectOption}
label={intl.formatMessage({
id: 'notification_settings.calls.calm',
defaultMessage: 'Calm',
})}
selected={callsMobileNotificationSound === Ringtone.Calm}
testID='notification_settings.calls.calm.option'
type='select'
value={Ringtone.Calm}
icon='volume-high'
iconColor={changeOpacity(theme.centerChannelColor, 0.56)}
/>
<SettingSeparator/>
<SettingOption
action={selectOption}
label={intl.formatMessage({
id: 'notification_settings.calls.urgent',
defaultMessage: 'Urgent',
})}
selected={callsMobileNotificationSound === Ringtone.Urgent}
testID='notification_settings.calls.urgent.option'
type='select'
value={Ringtone.Urgent}
icon='volume-high'
iconColor={changeOpacity(theme.centerChannelColor, 0.56)}
/>
<SettingSeparator/>
<SettingOption
action={selectOption}
label={intl.formatMessage({
id: 'notification_settings.calls.cheerful',
defaultMessage: 'Cheerful',
})}
selected={callsMobileNotificationSound === Ringtone.Cheerful}
testID='notification_settings.calls.cheerful.option'
type='select'
value={Ringtone.Cheerful}
icon='volume-high'
iconColor={changeOpacity(theme.centerChannelColor, 0.56)}
/>
<SettingSeparator/>
</SettingBlock>
)}
</SettingContainer>
);
};
export default NotificationCall;

View file

@ -2,11 +2,13 @@
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {getCallsConfig} from '@calls/state';
import SettingContainer from '@components/settings/container';
import SettingItem from '@components/settings/item';
import {General, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {t} from '@i18n';
import {popTopScreen} from '@screens/navigation';
@ -16,16 +18,24 @@ import {getEmailInterval, getEmailIntervalTexts, getNotificationProps} from '@ut
import type UserModel from '@typings/database/models/servers/user';
import type {AvailableScreens} from '@typings/screens/navigation';
const mentionTexts = {
const mentionTexts = defineMessages({
crtOn: {
id: t('notification_settings.mentions'),
id: 'notification_settings.mentions',
defaultMessage: 'Mentions',
},
crtOff: {
id: t('notification_settings.mentions_replies'),
id: 'notification_settings.mentions_replies',
defaultMessage: 'Mentions and Replies',
},
};
callsOn: {
id: 'notification_settings.calls_on',
defaultMessage: 'On',
},
callsOff: {
id: 'notification_settings.calls_off',
defaultMessage: 'Off',
},
});
type NotificationsProps = {
componentId: AvailableScreens;
@ -46,7 +56,9 @@ const Notifications = ({
sendEmailNotifications,
}: NotificationsProps) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const notifyProps = useMemo(() => getNotificationProps(currentUser), [currentUser?.notifyProps]);
const callsRingingEnabled = useMemo(() => getCallsConfig(serverUrl).EnableRinging, [serverUrl]);
const emailIntervalPref = useMemo(() =>
getEmailInterval(
@ -75,6 +87,18 @@ const Notifications = ({
gotoSettingsScreen(screen, title);
}, []);
const callsNotificationsOn = useMemo(() => Boolean(notifyProps?.calls_mobile_sound ? notifyProps.calls_mobile_sound === 'true' : notifyProps?.calls_desktop_sound === 'true'),
[notifyProps]);
const goToNotificationSettingsCall = useCallback(() => {
const screen = Screens.SETTINGS_NOTIFICATION_CALL;
const title = intl.formatMessage({
id: 'notification_settings.call_notification',
defaultMessage: 'Call Notifications',
});
gotoSettingsScreen(screen, title);
}, []);
const goToNotificationAutoResponder = useCallback(() => {
const screen = Screens.SETTINGS_NOTIFICATION_AUTO_RESPONDER;
const title = intl.formatMessage({
@ -112,6 +136,17 @@ const Notifications = ({
onPress={goToNotificationSettingsPush}
testID='notification_settings.push_notifications.option'
/>
{callsRingingEnabled &&
<SettingItem
optionName='call_notification'
onPress={goToNotificationSettingsCall}
info={intl.formatMessage({
id: callsNotificationsOn ? mentionTexts.callsOn.id : mentionTexts.callsOff.id,
defaultMessage: callsNotificationsOn ? mentionTexts.callsOn.defaultMessage : mentionTexts.callsOff.defaultMessage,
})}
testID='notification_settings.call_notifications.option'
/>
}
<SettingItem
optionName='email'
onPress={goToEmailSettings}

View file

@ -5,6 +5,7 @@ import moment from 'moment-timezone';
import {Alert} from 'react-native';
import {General, Permissions, Preferences} from '@constants';
import {Ringtone} from '@constants/calls';
import {CustomStatusDurationEnum} from '@constants/custom_status';
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
import {toTitleCase} from '@utils/helpers';
@ -339,6 +340,10 @@ export function getNotificationProps(user?: UserModel) {
push_status: 'online',
push_threads: 'all',
email_threads: 'all',
calls_desktop_sound: 'true',
calls_notification_sound: Ringtone.Calm,
calls_mobile_sound: '',
calls_mobile_notification_sound: '',
};
return props;

View file

@ -791,6 +791,16 @@
"notification_settings.auto_responder.footer.message": "Set a custom message that is automatically sent in response to direct messages, such as an out of office or vacation reply. Enabling this setting changes your status to Out of Office and disables notifications.",
"notification_settings.auto_responder.message": "Message",
"notification_settings.auto_responder.to.enable": "Enable automatic replies",
"notification_settings.call_notification": "Call Notifications",
"notification_settings.calls": "Call Notifications",
"notification_settings.calls_off": "Off",
"notification_settings.calls_on": "On",
"notification_settings.calls.callsInfo": "Note: silent mode must be off to hear the ringtone preview.",
"notification_settings.calls.calm": "Calm",
"notification_settings.calls.cheerful": "Cheerful",
"notification_settings.calls.dynamic": "Dynamic",
"notification_settings.calls.enable_sound": "Notification sound for incoming calls",
"notification_settings.calls.urgent": "Urgent",
"notification_settings.email": "Email Notifications",
"notification_settings.email.crt.emailInfo": "When enabled, any reply to a thread you're following will send an email notification",
"notification_settings.email.crt.send": "Thread reply notifications",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -648,7 +648,7 @@ platform :ios do
app_name_sub = app_name.gsub(" ", "_")
config_mode = ENV['BUILD_FOR_RELEASE'] == 'true' ? 'Release' : 'Debug'
method = ENV['IOS_BUILD_EXPORT_METHOD'].nil? || ENV['IOS_BUILD_EXPORT_METHOD'].empty? ? 'ad-hoc' : ENV['IOS_BUILD_EXPORT_METHOD']
# Need to add xcargs to only notification and share extensions
xcargs = ENV['SENTRY_ENABLED'] == 'true' ? "SENTRY_DSN_IOS='#{ENV['SENTRY_DSN_IOS']}' SENTRY_ENABLED='#{ENV['SENTRY_ENABLED']}'" : ''
@ -812,6 +812,8 @@ platform :android do
sh 'cp -R ../dist/assets/release/icons/android/* ../android/app/src/main/res/'
sh 'cp -R ../dist/assets/release/splash_screen/android/* ../android/app/src/main/res/'
end
sh 'mkdir -p ../android/app/src/main/res/raw/'
sh 'cp -R ../assets/sounds/* ../android/app/src/main/res/raw/'
end
lane :deploy do |options|
@ -877,7 +879,7 @@ platform :android do
if build_task.include?("assemble")
apks = lane_context[SharedValues::GRADLE_ALL_APK_OUTPUT_PATHS]
json_path = lane_context[SharedValues::GRADLE_OUTPUT_JSON_OUTPUT_PATH]
if json_path.nil? || json_path.empty?
dir_name = File.dirname(apks.first)
json_path = Dir.glob(File.join(dir_name, '*.json')).first

View file

@ -169,6 +169,10 @@
7FD482682864DC5900A5B18B /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */; };
7FD482692864DC5900A5B18B /* OpenSans-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0DB14DFDF6E04FA69FE769DC /* OpenSans-SemiBoldItalic.ttf */; };
7FF9C03D2983E7C6005CDCF5 /* ErrorSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF9C03C2983E7C6005CDCF5 /* ErrorSharingView.swift */; };
83ABFD122C1C90D90029685B /* calls_dynamic.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 83ABFD0E2C1C90D90029685B /* calls_dynamic.mp3 */; };
83ABFD132C1C90D90029685B /* calls_cheerful.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 83ABFD0F2C1C90D90029685B /* calls_cheerful.mp3 */; };
83ABFD142C1C90D90029685B /* calls_calm.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 83ABFD102C1C90D90029685B /* calls_calm.mp3 */; };
83ABFD152C1C90D90029685B /* calls_urgent.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 83ABFD112C1C90D90029685B /* calls_urgent.mp3 */; };
A94508A396424B2DB778AFE9 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */; };
C84AE74C57DDE97D6C3D6924 /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A83BBA09DE89A630126EA06 /* libPods-Mattermost.a */; };
C9A107102BBD7C8700753CDC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C9A1070F2BBD7C8700753CDC /* PrivacyInfo.xcprivacy */; };
@ -369,6 +373,10 @@
7FFE32BE1FD9CCAA0038C7A0 /* SDWebImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SDWebImage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Sentry.framework; sourceTree = BUILT_PRODUCTS_DIR; };
81061F4CBB31484A94D5A8EE /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
83ABFD0E2C1C90D90029685B /* calls_dynamic.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = calls_dynamic.mp3; path = ../assets/sounds/calls_dynamic.mp3; sourceTree = "<group>"; };
83ABFD0F2C1C90D90029685B /* calls_cheerful.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = calls_cheerful.mp3; path = ../assets/sounds/calls_cheerful.mp3; sourceTree = "<group>"; };
83ABFD102C1C90D90029685B /* calls_calm.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = calls_calm.mp3; path = ../assets/sounds/calls_calm.mp3; sourceTree = "<group>"; };
83ABFD112C1C90D90029685B /* calls_urgent.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = calls_urgent.mp3; path = ../assets/sounds/calls_urgent.mp3; sourceTree = "<group>"; };
BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Regular.ttf"; path = "../assets/fonts/OpenSans-Regular.ttf"; sourceTree = "<group>"; };
BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-LightItalic.ttf"; path = "../assets/fonts/OpenSans-LightItalic.ttf"; sourceTree = "<group>"; };
C9A1070F2BBD7C8700753CDC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
@ -464,6 +472,10 @@
FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */,
41F3AFE83AAF4B74878AB78A /* OpenSans-Italic.ttf */,
6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */,
83ABFD102C1C90D90029685B /* calls_calm.mp3 */,
83ABFD0F2C1C90D90029685B /* calls_cheerful.mp3 */,
83ABFD0E2C1C90D90029685B /* calls_dynamic.mp3 */,
83ABFD112C1C90D90029685B /* calls_urgent.mp3 */,
BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */,
BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */,
E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */,
@ -1202,6 +1214,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
83ABFD132C1C90D90029685B /* calls_cheerful.mp3 in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
7F0F4B0A24BA173900E14C60 /* LaunchScreen.storyboard in Resources */,
7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */,
@ -1215,6 +1228,9 @@
7FB31F882710996D0032E2E5 /* OpenSans-Italic.ttf in Resources */,
7FB31F892710996D0032E2E5 /* OpenSans-Light.ttf in Resources */,
7FB31F8A2710996D0032E2E5 /* OpenSans-LightItalic.ttf in Resources */,
83ABFD122C1C90D90029685B /* calls_dynamic.mp3 in Resources */,
83ABFD152C1C90D90029685B /* calls_urgent.mp3 in Resources */,
83ABFD142C1C90D90029685B /* calls_calm.mp3 in Resources */,
7FB31F8B2710996D0032E2E5 /* OpenSans-Regular.ttf in Resources */,
7FB31F8E2710996D0032E2E5 /* compass-icons.ttf in Resources */,
58495E36BF1A4EAB93609E57 /* Metropolis-SemiBold.ttf in Resources */,

View file

@ -0,0 +1,335 @@
diff --git a/node_modules/react-native-incall-manager/android/src/main/java/com/zxcpoiu/incallmanager/InCallManagerModule.java b/node_modules/react-native-incall-manager/android/src/main/java/com/zxcpoiu/incallmanager/InCallManagerModule.java
index 10cb1e5..ba5d918 100644
--- a/node_modules/react-native-incall-manager/android/src/main/java/com/zxcpoiu/incallmanager/InCallManagerModule.java
+++ b/node_modules/react-native-incall-manager/android/src/main/java/com/zxcpoiu/incallmanager/InCallManagerModule.java
@@ -1040,107 +1040,107 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
}
@ReactMethod
- public void startRingtone(final String ringtoneUriType, final int seconds) {
- Thread thread = new Thread() {
- @Override
- public void run() {
+ public void startRingtone(final String ringtoneUriType, final int seconds, Promise promise) {
+ try {
+ Log.d(TAG, "startRingtone(): UriType=" + ringtoneUriType);
+ if (mRingtone != null) {
+ if (mRingtone.isPlaying()) {
+ Log.d(TAG, "startRingtone(): is already playing");
+ promise.reject("ringtone is already playing", "no error");
+ return;
+ }
try {
- Looper.prepare();
-
- Log.d(TAG, "startRingtone(): UriType=" + ringtoneUriType);
- if (mRingtone != null) {
- if (mRingtone.isPlaying()) {
- Log.d(TAG, "startRingtone(): is already playing");
- return;
- } else {
- stopRingtone(); // --- use brandnew instance
- }
- }
+ stopRingtone(); // ensure no ringtone is currently playing
+ } catch (Exception e) {
+ promise.reject("stopRingtone() failed", e);
+ return;
+ }
+ }
- //if (!audioManager.isStreamMute(AudioManager.STREAM_RING)) {
- //if (origRingerMode == AudioManager.RINGER_MODE_NORMAL) {
- if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
- Log.d(TAG, "startRingtone(): ringer is silent. leave without play.");
- return;
- }
+ if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
+ Log.d(TAG, "startRingtone(): ringer is silent. leave without play.");
+ promise.reject("ringer is silent. leave without playing", "no error");
+ return;
+ }
- // --- there is no _DTMF_ option in startRingtone()
- Uri ringtoneUri = getRingtoneUri(ringtoneUriType);
- if (ringtoneUri == null) {
- Log.d(TAG, "startRingtone(): no available media");
- return;
- }
+ // --- there is no _DTMF_ option in startRingtone()
+ Uri ringtoneUri = getRingtoneUri(ringtoneUriType);
+ if (ringtoneUri == null) {
+ Log.d(TAG, "startRingtone(): no available media");
+ promise.reject("no available media", "no error");
+ return;
+ }
- if (audioManagerActivated) {
- InCallManagerModule.this.stop();
- }
+ if (audioManagerActivated) {
+ InCallManagerModule.this.stop();
+ }
- wakeLockUtils.acquirePartialWakeLock();
+ wakeLockUtils.acquirePartialWakeLock();
- storeOriginalAudioSetup();
- Map data = new HashMap<String, Object>();
- mRingtone = new myMediaPlayer();
+ storeOriginalAudioSetup();
+ Map data = new HashMap<String, Object>();
+ mRingtone = new myMediaPlayer();
- data.put("name", "mRingtone");
- data.put("sourceUri", ringtoneUri);
- data.put("setLooping", true);
+ data.put("name", "mRingtone");
+ data.put("sourceUri", ringtoneUri);
+ data.put("setLooping", true);
- //data.put("audioStream", AudioManager.STREAM_RING); // --- lagacy
- data.put("audioUsage", AudioAttributes.USAGE_NOTIFICATION_RINGTONE); // --- USAGE_NOTIFICATION_COMMUNICATION_REQUEST?
- data.put("audioContentType", AudioAttributes.CONTENT_TYPE_MUSIC);
+ //data.put("audioStream", AudioManager.STREAM_RING); // --- lagacy
+ data.put("audioUsage", AudioAttributes.USAGE_NOTIFICATION_RINGTONE); // --- USAGE_NOTIFICATION_COMMUNICATION_REQUEST?
+ data.put("audioContentType", AudioAttributes.CONTENT_TYPE_MUSIC);
- setMediaPlayerEvents((MediaPlayer) mRingtone, "mRingtone");
+ setMediaPlayerEvents((MediaPlayer) mRingtone, "mRingtone");
- mRingtone.startPlay(data);
+ mRingtone.startPlay(data);
- if (seconds > 0) {
- mRingtoneCountDownHandler = new Handler();
- mRingtoneCountDownHandler.postDelayed(new Runnable() {
- public void run() {
- try {
- Log.d(TAG, String.format("mRingtoneCountDownHandler.stopRingtone() timeout after %d seconds", seconds));
- stopRingtone();
- } catch(Exception e) {
- Log.d(TAG, "mRingtoneCountDownHandler.stopRingtone() failed.");
- }
- }
- }, seconds * 1000);
+ if (seconds > 0) {
+ mRingtoneCountDownHandler = new Handler();
+ mRingtoneCountDownHandler.postDelayed(() -> {
+ try {
+ Log.d(TAG, String.format("mRingtoneCountDownHandler.stopRingtone() timeout after %d seconds", seconds));
+ stopRingtone();
+ } catch (Exception e) {
+ Log.d(TAG, "mRingtoneCountDownHandler.stopRingtone() failed.");
}
-
- Looper.loop();
- } catch(Exception e) {
- wakeLockUtils.releasePartialWakeLock();
- Log.e(TAG, "startRingtone() failed", e);
- }
+ }, seconds * 1000);
}
- };
+ } catch (Exception e) {
+ wakeLockUtils.releasePartialWakeLock();
+ Log.e(TAG, "startRingtone() failed", e);
+ promise.reject("startRingtone() failed", e);
+ }
- thread.start();
+ promise.resolve(null);
}
+
@ReactMethod
- public void stopRingtone() {
- Thread thread = new Thread() {
- @Override
- public void run() {
- try {
- if (mRingtone != null) {
- mRingtone.stopPlay();
- mRingtone = null;
- restoreOriginalAudioSetup();
- }
- if (mRingtoneCountDownHandler != null) {
- mRingtoneCountDownHandler.removeCallbacksAndMessages(null);
- mRingtoneCountDownHandler = null;
- }
- } catch (Exception e) {
- Log.d(TAG, "stopRingtone() failed");
- }
- wakeLockUtils.releasePartialWakeLock();
- }
- };
+ public void stopRingtone(Promise promise) {
+ try {
+ stopRingtone();
+ } catch (Exception e) {
+ promise.reject("stopRingtone() failed", e);
+ }
+ promise.resolve(null);
+ }
- thread.start();
+ private void stopRingtone() {
+ try {
+ if (mRingtone != null) {
+ mRingtone.stopPlay();
+ mRingtone = null;
+ restoreOriginalAudioSetup();
+ }
+ if (mRingtoneCountDownHandler != null) {
+ mRingtoneCountDownHandler.removeCallbacksAndMessages(null);
+ mRingtoneCountDownHandler = null;
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "stopRingtone() failed");
+ wakeLockUtils.releasePartialWakeLock();
+ throw e;
+ }
+ wakeLockUtils.releasePartialWakeLock();
}
private void setMediaPlayerEvents(MediaPlayer mp, final String name) {
@@ -1267,6 +1267,32 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
return getAudioUri(type, fileBundle, fileBundleExt, fileSysWithExt, fileSysPath, "bundleBusytoneUri", "defaultBusytoneUri");
}
+ private Uri getRingtoneUriFromBundle(final String filename) {
+ if (audioUriMap.containsKey(filename)) {
+ Log.d(TAG, "getAudioUri() using: " + filename);
+ return audioUriMap.get(filename);
+ }
+
+ int res = 0;
+ ReactContext reactContext = getReactApplicationContext();
+ if (reactContext == null) {
+ Log.d(TAG, "getAudioUri() reactContext is null");
+ } else {
+ res = reactContext.getResources().getIdentifier(filename, "raw", mPackageName);
+ }
+
+ if (res <= 0) {
+ Log.d(TAG, String.format("getAudioUri() %s.%s not found in bundle.", filename, "mp3"));
+ audioUriMap.put(filename, null);
+ return null;
+ }
+
+ Uri uri = Uri.parse("android.resource://" + mPackageName + "/" + Integer.toString(res));
+ audioUriMap.put(filename, uri);
+ Log.d(TAG, "getAudioUri() using: " + filename);
+ return uri;
+ }
+
private Uri getAudioUri(final String _type, final String fileBundle, final String fileBundleExt, final String fileSysWithExt, final String fileSysPath, final String uriBundle, final String uriDefault) {
String type = _type;
if (type.equals("_BUNDLE_")) {
@@ -1300,6 +1326,12 @@ public class InCallManagerModule extends ReactContextBaseJavaModule implements L
final String target = fileSysPath + "/" + type;
Uri _uri = getSysFileUri(target);
if (_uri == null) {
+ Log.d(TAG, "getAudioUri() file not found in system, trying bundle");
+ _uri = getRingtoneUriFromBundle(type);
+ if (_uri != null) {
+ return _uri;
+ }
+
Log.d(TAG, "getAudioUri() using user default");
return getDefaultUserUri(uriDefault);
} else {
diff --git a/node_modules/react-native-incall-manager/index.d.ts b/node_modules/react-native-incall-manager/index.d.ts
index c8ebe9d..5f2ab9e 100644
--- a/node_modules/react-native-incall-manager/index.d.ts
+++ b/node_modules/react-native-incall-manager/index.d.ts
@@ -37,9 +37,9 @@ declare class InCallManager {
vibrate_pattern: number | number[],
ios_category: string,
seconds: number
- ): void;
+ ): Promise<void>;
- stopRingtone(): void;
+ stopRingtone(): Promise<void>;
startProximitySensor(): void;
diff --git a/node_modules/react-native-incall-manager/index.js b/node_modules/react-native-incall-manager/index.js
index ac5cafb..6887150 100644
--- a/node_modules/react-native-incall-manager/index.js
+++ b/node_modules/react-native-incall-manager/index.js
@@ -73,14 +73,14 @@ class InCallManager {
_InCallManager.setMicrophoneMute(enable);
}
- startRingtone(ringtone, vibrate_pattern, ios_category, seconds) {
+ async startRingtone(ringtone, vibrate_pattern, ios_category, seconds) {
ringtone = (typeof ringtone === 'string') ? ringtone : "_DEFAULT_";
this.vibrate = (Array.isArray(vibrate_pattern)) ? true : false;
ios_category = (ios_category === 'playback') ? 'playback' : "default";
seconds = (typeof seconds === 'number' && seconds > 0) ? parseInt(seconds) : -1; // --- android only, default looping
if (Platform.OS === 'android') {
- _InCallManager.startRingtone(ringtone, seconds);
+ await _InCallManager.startRingtone(ringtone, seconds);
} else {
_InCallManager.startRingtone(ringtone, ios_category);
}
@@ -91,11 +91,11 @@ class InCallManager {
}
}
- stopRingtone() {
+ async stopRingtone() {
if (this.vibrate) {
Vibration.cancel();
}
- _InCallManager.stopRingtone();
+ await _InCallManager.stopRingtone();
}
startProximitySensor() {
diff --git a/node_modules/react-native-incall-manager/ios/RNInCallManager/RNInCallManager.m b/node_modules/react-native-incall-manager/ios/RNInCallManager/RNInCallManager.m
index 7c54f6e..f861a0f 100644
--- a/node_modules/react-native-incall-manager/ios/RNInCallManager/RNInCallManager.m
+++ b/node_modules/react-native-incall-manager/ios/RNInCallManager/RNInCallManager.m
@@ -1126,6 +1126,18 @@ - (NSURL *)getRingtoneUri:(NSString *)_type
return uri;
}
+
+- (NSURL *)getRingtoneUriFromBundle:(NSString *)filename
+{
+ NSString *fileBundleExt = @"mp3";
+ NSLog(@"RNInCallManager.getRingtoneUriFromBundle(): trying to get from bundle: %@.%@", filename, fileBundleExt);
+ NSURL *uriBundle = [[NSBundle mainBundle] URLForResource:filename withExtension:fileBundleExt];
+ if (uriBundle == nil) {
+ NSLog(@"RNInCallManager.getRingtoneUriFromBundle(): not found in bundle: %@.%@", filename, fileBundleExt);
+ }
+ return uriBundle;
+}
+
- (NSURL *)getAudioUri:(NSString *)_type
fileBundle:(NSString *)fileBundle
fileBundleExt:(NSString *)fileBundleExt
@@ -1149,11 +1161,15 @@ - (NSURL *)getAudioUri:(NSString *)_type
}
}
- if (*uriDefault == nil) {
- NSString *target = [NSString stringWithFormat:@"%@/%@", fileSysPath, type];
- *uriDefault = [self getSysFileUri:target];
+ // --- Check file every time in case user deleted.
+ NSString *target = [NSString stringWithFormat:@"%@/%@", fileSysPath, type];
+ NSURL *uri = [self getSysFileUri:target];
+
+ if (uri == nil) {
+ uri = [self getRingtoneUriFromBundle:type];
}
- return *uriDefault;
+
+ return uri;
}
- (NSURL *)getSysFileUri:(NSString *)target

View file

@ -34,4 +34,13 @@ if [ -z "$ASSETS" ]; then
exit 1
else
echo "Generating app assets"
fi
fi
SOUNDS="assets/sounds"
if [ -z "$SOUNDS" ]; then
echo "Sound assets not found"
exit 1
else
echo "Copying sound assets for bundling"
cp $SOUNDS/* "android/app/src/main/res/raw/"
fi

View file

@ -11,6 +11,7 @@ import nock from 'nock';
import Config from '@assets/config.json';
import {Client} from '@client/rest';
import {ActionType} from '@constants';
import {Ringtone} from '@constants/calls';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import DatabaseManager from '@database/manager';
@ -429,6 +430,10 @@ class TestHelper {
mention_keys: '',
push: 'default',
push_status: 'away',
calls_desktop_sound: 'true',
calls_mobile_notification_sound: '',
calls_mobile_sound: 'true',
calls_notification_sound: '',
};
};
@ -453,6 +458,10 @@ class TestHelper {
channel: 'true',
mention_keys: '',
highlight_keys: '',
calls_desktop_sound: 'true',
calls_mobile_sound: '',
calls_notification_sound: Ringtone.Calm,
calls_mobile_notification_sound: '',
},
last_picture_update: 0,
position: '',

View file

@ -19,6 +19,10 @@ type UserNotifyProps = {
user_id?: string;
push_threads?: 'all' | 'mention';
email_threads?: 'all' | 'mention';
calls_desktop_sound: 'true' | 'false';
calls_notification_sound: string;
calls_mobile_sound: 'true' | 'false' | '';
calls_mobile_notification_sound: string;
};
type UserProfile = {

View file

@ -44,9 +44,9 @@ declare module 'react-native-incall-manager' {
vibrate_pattern?: any[],
ios_category?: string,
seconds?: number
): void;
): Promise<void>;
stopRingtone(): void;
stopRingtone(): Promise<void>;
startProximitySensor(): void;