feat (Android): mobile ringer (#5286)

This commit is contained in:
Gleidson Daniel Silva 2023-11-24 09:46:58 -03:00 committed by GitHub
parent bd17ee55bf
commit 31ed940426
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 335 additions and 36 deletions

Binary file not shown.

View File

@ -97,9 +97,13 @@ public class CustomPushNotification extends PushNotification {
bundle.putString("senderId", hasSender ? loadedEjson.sender._id : "1");
bundle.putString("avatarUri", loadedEjson.getAvatarUri());
notificationMessages.get(notId).add(bundle);
postNotification(Integer.parseInt(notId));
notifyReceivedToJS();
if (loadedEjson.notificationType.equals("videoconf")) {
notifyReceivedToJS();
} else {
notificationMessages.get(notId).add(bundle);
postNotification(Integer.parseInt(notId));
notifyReceivedToJS();
}
}
@Override

View File

@ -65,7 +65,7 @@ export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DI
export const LOGOUT = 'LOGOUT'; // logout is always success
export const DELETE_ACCOUNT = 'DELETE_ACCOUNT';
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN', 'OPEN_VIDEO_CONF']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'CLEAR']);

View File

@ -22,3 +22,10 @@ export function deepLinkingOpen(params: Partial<IParams>): IDeepLinkingOpen {
params
};
}
export function deepLinkingClickCallPush(params: any): IDeepLinkingOpen {
return {
type: DEEP_LINKING.OPEN_VIDEO_CONF,
params
};
}

View File

@ -8,8 +8,6 @@ export enum ERingerSounds {
}
const Ringer = React.memo(({ ringer }: { ringer: ERingerSounds }) => {
console.log('Ringer', ringer);
const sound = useRef<Audio.Sound | null>(null);
useEffect(() => {
(async () => {

View File

@ -749,6 +749,7 @@
"Continue": "Continue",
"Message_has_been_shared": "Message has been shared",
"No_channels_in_team": "No Channels on this team",
"conference_call": "Conference call",
"Room_not_found": "Room not found",
"The_room_does_not_exist": "The room does not exist or you may not have access permission",
"Supported_versions_expired_title": "{{workspace_name}} is running an unsupported version of Rocket.Chat",
@ -758,5 +759,6 @@
"The_user_will_be_able_to_type_in_roomName": "The user will be able to type in {{roomName}}",
"Enable_writing_in_room": "Enable writing in room",
"Disable_writing_in_room": "Disable writing in room",
"Pinned_a_message": "Pinned a message:"
"Pinned_a_message": "Pinned a message:",
"Missed_call": "Missed call"
}

View File

@ -747,7 +747,6 @@
"decline": "Recusar",
"accept": "Aceitar",
"Incoming_call_from": "Chamada recebida de",
"Call_started": "Chamada Iniciada",
"Room_not_found": "Sala não encontrada",
"The_room_does_not_exist": "A sala não existe ou você pode não ter permissão de acesso",
"Call_started": "Chamada iniciada",
@ -757,5 +756,6 @@
"The_user_wont_be_able_to_type_in_roomName": "O usuário não poderá digitar em {{roomName}}",
"The_user_will_be_able_to_type_in_roomName": "O usuário poderá digitar em {{roomName}}",
"Enable_writing_in_room": "Permitir escrita na sala",
"Disable_writing_in_room": "Desabilitar escrita na sala"
"Disable_writing_in_room": "Desabilitar escrita na sala",
"Missed_call": "Chamada perdida"
}

View File

@ -1,30 +1,26 @@
import React from 'react';
import { Dimensions, Linking } from 'react-native';
import { initialWindowMetrics, SafeAreaProvider } from 'react-native-safe-area-context';
import RNScreens from 'react-native-screens';
import { Provider } from 'react-redux';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Orientation from 'react-native-orientation-locker';
import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context';
import RNScreens from 'react-native-screens';
import { Provider } from 'react-redux';
import AppContainer from './AppContainer';
import { appInit, appInitLocalSettings, setMasterDetail as setMasterDetailAction } from './actions/app';
import { deepLinkingOpen } from './actions/deepLinking';
import AppContainer from './AppContainer';
import { ActionSheetProvider } from './containers/ActionSheet';
import InAppNotification from './containers/InAppNotification';
import Loading from './containers/Loading';
import Toast from './containers/Toast';
import TwoFactor from './containers/TwoFactor';
import Loading from './containers/Loading';
import { IThemePreference } from './definitions/ITheme';
import { DimensionsContext } from './dimensions';
import { colors, isFDroidBuild, MIN_WIDTH_MASTER_DETAIL_LAYOUT, themes } from './lib/constants';
import { MIN_WIDTH_MASTER_DETAIL_LAYOUT, colors, isFDroidBuild, themes } from './lib/constants';
import { getAllowAnalyticsEvents, getAllowCrashReport } from './lib/methods';
import parseQuery from './lib/methods/helpers/parseQuery';
import { initializePushNotifications, onNotification } from './lib/notifications';
import store from './lib/store';
import { initStore } from './lib/store/auxStore';
import { ThemeContext, TSupportedThemes } from './theme';
import { debounce, isTablet } from './lib/methods/helpers';
import { toggleAnalyticsEventsReport, toggleCrashErrorsReport } from './lib/methods/helpers/log';
import parseQuery from './lib/methods/helpers/parseQuery';
import {
getTheme,
initialTheme,
@ -33,6 +29,11 @@ import {
subscribeTheme,
unsubscribeTheme
} from './lib/methods/helpers/theme';
import { initializePushNotifications, onNotification } from './lib/notifications';
import { getInitialNotification } from './lib/notifications/videoConf/getInitialNotification';
import store from './lib/store';
import { initStore } from './lib/store/auxStore';
import { TSupportedThemes, ThemeContext } from './theme';
import ChangePasscodeView from './views/ChangePasscodeView';
import ScreenLockedView from './views/ScreenLockedView';
@ -126,6 +127,8 @@ export default class Root extends React.Component<{}, IState> {
return;
}
await getInitialNotification();
// Open app from deep linking
const deepLinking = await Linking.getInitialURL();
const parsedDeepLinkingURL = parseDeepLinking(deepLinking!);

View File

@ -1,3 +1,5 @@
export const BACKGROUND_PUSH_COLOR = '#F5455C';
export const STATUS_COLORS: any = {
online: '#2de0a5',
busy: '#f5455c',

View File

@ -118,3 +118,5 @@ export const goRoom = async ({
return navigate({ item, isMasterDetail, popToRoot, ...props });
};
export const navigateToRoom = navigate;

View File

@ -26,7 +26,7 @@ export const handleAndroidBltPermission = async (): Promise<void> => {
}
};
export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean): Promise<void> => {
export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean, fromPush?: boolean): Promise<void> => {
try {
const result = await Services.videoConferenceJoin(callId, cam, mic);
if (result.success) {
@ -38,7 +38,11 @@ export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean
}
}
} catch (e) {
showErrorAlert(i18n.t('error-init-video-conf'));
if (fromPush) {
showErrorAlert(i18n.t('Missed_call'));
} else {
showErrorAlert(i18n.t('error-init-video-conf'));
}
log(e);
}
};

View File

@ -1,10 +1,10 @@
import EJSON from 'ejson';
import { store } from '../store/auxStore';
import { deepLinkingOpen } from '../../actions/deepLinking';
import { isFDroidBuild } from '../constants';
import { deviceToken, pushNotificationConfigure, setNotificationsBadgeCount, removeAllNotifications } from './push';
import { INotification, SubscriptionType } from '../../definitions';
import { isFDroidBuild } from '../constants';
import { store } from '../store/auxStore';
import { deviceToken, pushNotificationConfigure, removeAllNotifications, setNotificationsBadgeCount } from './push';
interface IEjson {
rid: string;

View File

@ -0,0 +1,122 @@
import notifee, { AndroidCategory, AndroidImportance, AndroidVisibility, Event } from '@notifee/react-native';
import messaging from '@react-native-firebase/messaging';
import AsyncStorage from '@react-native-async-storage/async-storage';
import ejson from 'ejson';
import { deepLinkingClickCallPush } from '../../../actions/deepLinking';
import i18n from '../../../i18n';
import { BACKGROUND_PUSH_COLOR } from '../../constants';
import { store } from '../../store/auxStore';
const VIDEO_CONF_CHANNEL = 'video-conf-call';
const VIDEO_CONF_TYPE = 'videoconf';
interface Caller {
_id?: string;
name?: string;
}
interface NotificationData {
notificationType?: string;
status?: number;
rid?: string;
caller?: Caller;
}
const createChannel = () =>
notifee.createChannel({
id: VIDEO_CONF_CHANNEL,
name: 'Video Call',
lights: true,
vibration: true,
importance: AndroidImportance.HIGH,
sound: 'ringtone'
});
const handleBackgroundEvent = async (event: Event) => {
const { pressAction, notification } = event.detail;
const notificationData = notification?.data;
if (
typeof notificationData?.caller === 'object' &&
(notificationData.caller as Caller)?._id &&
(event.type === 1 || event.type === 2)
) {
if (store?.getState()?.app.ready) {
store.dispatch(deepLinkingClickCallPush({ ...notificationData, event: pressAction?.id }));
} else {
AsyncStorage.setItem('pushNotification', JSON.stringify({ ...notificationData, event: pressAction?.id }));
}
await notifee.cancelNotification(
`${notificationData.rid}${(notificationData.caller as Caller)._id}`.replace(/[^A-Za-z0-9]/g, '')
);
}
};
const backgroundNotificationHandler = () => {
notifee.onBackgroundEvent(handleBackgroundEvent);
};
const displayVideoConferenceNotification = async (notification: NotificationData) => {
const id = `${notification.rid}${notification.caller?._id}`.replace(/[^A-Za-z0-9]/g, '');
const actions = [
{
title: i18n.t('accept'),
pressAction: {
id: 'accept',
launchActivity: 'default'
}
},
{
title: i18n.t('decline'),
pressAction: {
id: 'decline',
launchActivity: 'default'
}
}
];
await notifee.displayNotification({
id,
title: i18n.t('conference_call'),
body: `${i18n.t('Incoming_call_from')} ${notification.caller?.name}`,
data: notification as { [key: string]: string | number | object },
android: {
channelId: VIDEO_CONF_CHANNEL,
category: AndroidCategory.CALL,
visibility: AndroidVisibility.PUBLIC,
importance: AndroidImportance.HIGH,
smallIcon: 'ic_notification',
color: BACKGROUND_PUSH_COLOR,
actions,
lightUpScreen: true,
loopSound: true,
sound: 'ringtone',
autoCancel: false,
ongoing: true,
pressAction: {
id: 'default',
launchActivity: 'default'
}
}
});
};
const setBackgroundNotificationHandler = () => {
createChannel();
messaging().setBackgroundMessageHandler(async message => {
const notification: NotificationData = ejson.parse(message?.data?.ejson as string);
if (notification?.notificationType === VIDEO_CONF_TYPE) {
if (notification.status === 0) {
await displayVideoConferenceNotification(notification);
} else if (notification.status === 4) {
const id = `${notification.rid}${notification.caller?._id}`.replace(/[^A-Za-z0-9]/g, '');
await notifee.cancelNotification(id);
}
}
return null;
});
};
setBackgroundNotificationHandler();
backgroundNotificationHandler();

View File

@ -0,0 +1,15 @@
import { deepLinkingClickCallPush } from '../../../actions/deepLinking';
import { isAndroid } from '../../methods/helpers';
import { store } from '../../store/auxStore';
export const getInitialNotification = async (): Promise<void> => {
if (isAndroid) {
const notifee = require('@notifee/react-native').default;
const initialNotification = await notifee.getInitialNotification();
if (initialNotification?.notification?.data?.notificationType === 'videoconf') {
store.dispatch(
deepLinkingClickCallPush({ ...initialNotification?.notification?.data, event: initialNotification?.pressAction?.id })
);
}
}
};

View File

@ -1,20 +1,21 @@
import { all, delay, put, select, take, takeLatest } from 'redux-saga/effects';
import { all, call, delay, put, select, take, takeLatest } from 'redux-saga/effects';
import UserPreferences from '../lib/methods/userPreferences';
import * as types from '../actions/actionsTypes';
import { selectServerRequest, serverInitAdd } from '../actions/server';
import { inviteLinksRequest, inviteLinksSetToken } from '../actions/inviteLinks';
import database from '../lib/database';
import EventEmitter from '../lib/methods/helpers/events';
import { appInit, appStart } from '../actions/app';
import { localAuthenticate } from '../lib/methods/helpers/localAuthentication';
import { goRoom } from '../lib/methods/helpers/goRoom';
import { getUidDirectMessage } from '../lib/methods/helpers';
import { inviteLinksRequest, inviteLinksSetToken } from '../actions/inviteLinks';
import { loginRequest } from '../actions/login';
import log from '../lib/methods/helpers/log';
import { selectServerRequest, serverInitAdd } from '../actions/server';
import { RootEnum } from '../definitions';
import { CURRENT_SERVER, TOKEN_KEY } from '../lib/constants';
import database from '../lib/database';
import { canOpenRoom, getServerInfo } from '../lib/methods';
import { getUidDirectMessage } from '../lib/methods/helpers';
import EventEmitter from '../lib/methods/helpers/events';
import { goRoom, navigateToRoom } from '../lib/methods/helpers/goRoom';
import { localAuthenticate } from '../lib/methods/helpers/localAuthentication';
import log from '../lib/methods/helpers/log';
import UserPreferences from '../lib/methods/userPreferences';
import { videoConfJoin } from '../lib/methods/videoConf';
import { Services } from '../lib/services';
const roomTypes = {
@ -168,7 +169,90 @@ const handleOpen = function* handleOpen({ params }) {
}
};
const handleNavigateCallRoom = function* handleNavigateCallRoom({ params }) {
yield put(appStart({ root: RootEnum.ROOT_INSIDE }));
const db = database.active;
const subsCollection = db.get('subscriptions');
const room = yield subsCollection.find(params.rid);
if (room) {
const isMasterDetail = yield select(state => state.app.isMasterDetail);
yield navigateToRoom({ item: room, isMasterDetail, popToRoot: true });
const uid = params.caller._id;
const { rid, callId, event } = params;
if (event === 'accept') {
yield call(Services.notifyUser, `${uid}/video-conference`, {
action: 'accepted',
params: { uid, rid, callId }
});
yield videoConfJoin(callId, true, false, true);
} else if (event === 'decline') {
yield call(Services.notifyUser, `${uid}/video-conference`, {
action: 'rejected',
params: { uid, rid, callId }
});
}
}
};
const handleClickCallPush = function* handleOpen({ params }) {
const serversDB = database.servers;
const serversCollection = serversDB.get('servers');
let { host } = params;
if (host.slice(-1) === '/') {
host = host.slice(0, host.length - 1);
}
const [server, user] = yield all([
UserPreferences.getString(CURRENT_SERVER),
UserPreferences.getString(`${TOKEN_KEY}-${host}`)
]);
if (server === host && user) {
const connected = yield select(state => state.server.connected);
if (!connected) {
yield localAuthenticate(host);
yield put(selectServerRequest(host));
yield take(types.LOGIN.SUCCESS);
}
yield handleNavigateCallRoom({ params });
} else {
// search if deep link's server already exists
try {
const hostServerRecord = yield serversCollection.find(host);
if (hostServerRecord && user) {
yield localAuthenticate(host);
yield put(selectServerRequest(host, hostServerRecord.version, true, true));
yield take(types.LOGIN.SUCCESS);
yield handleNavigateCallRoom({ params });
return;
}
} catch (e) {
// do nothing?
}
// if deep link is from a different server
const result = yield Services.getServerInfo(host);
if (!result.success) {
// Fallback to prevent the app from being stuck on splash screen
yield fallbackNavigation();
return;
}
yield put(appStart({ root: RootEnum.ROOT_OUTSIDE }));
yield put(serverInitAdd(server));
yield delay(1000);
EventEmitter.emit('NewServer', { server: host });
if (params.token) {
yield take(types.SERVER.SELECT_SUCCESS);
yield put(loginRequest({ resume: params.token }, true));
yield take(types.LOGIN.SUCCESS);
yield handleNavigateCallRoom({ params });
}
}
};
const root = function* root() {
yield takeLatest(types.DEEP_LINKING.OPEN, handleOpen);
yield takeLatest(types.DEEP_LINKING.OPEN_VIDEO_CONF, handleClickCallPush);
};
export default root;

View File

@ -1,5 +1,6 @@
import { put, takeLatest } from 'redux-saga/effects';
import { call, put, takeLatest } from 'redux-saga/effects';
import RNBootSplash from 'react-native-bootsplash';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { BIOMETRY_ENABLED_KEY, CURRENT_SERVER, TOKEN_KEY } from '../lib/constants';
import UserPreferences from '../lib/methods/userPreferences';
@ -12,6 +13,7 @@ import { localAuthenticate } from '../lib/methods/helpers/localAuthentication';
import { appReady, appStart } from '../actions/app';
import { RootEnum } from '../definitions';
import { getSortPreferences } from '../lib/methods';
import { deepLinkingClickCallPush } from '../actions/deepLinking';
export const initLocalSettings = function* initLocalSettings() {
const sortPreferences = getSortPreferences();
@ -70,6 +72,11 @@ const restore = function* restore() {
}
yield put(appReady({}));
const pushNotification = yield call(AsyncStorage.getItem, 'pushNotification');
if (pushNotification) {
const pushNotification = yield call(AsyncStorage.removeItem, 'pushNotification');
yield call(deepLinkingClickCallPush, JSON.parse(pushNotification));
}
} catch (e) {
log(e);
yield put(appStart({ root: RootEnum.ROOT_OUTSIDE }));

View File

@ -3,6 +3,8 @@ import 'react-native-console-time-polyfill';
import { AppRegistry } from 'react-native';
import { name as appName, share as shareName } from './app.json';
import { isFDroidBuild } from './app/lib/constants';
import { isAndroid } from './app/lib/methods/helpers';
if (__DEV__) {
require('./app/ReactotronConfig');
@ -18,6 +20,11 @@ if (__DEV__) {
console.info = () => {};
}
if (!isFDroidBuild && isAndroid) {
require('./app/lib/notifications/videoConf/backgroundNotificationHandler');
}
AppRegistry.registerComponent(appName, () => require('./app/index').default);
AppRegistry.registerComponent(shareName, () => require('./app/share').default);

View File

@ -31,6 +31,7 @@
"@codler/react-native-keyboard-aware-scroll-view": "^2.0.1",
"@gorhom/bottom-sheet": "^4.3.1",
"@hookform/resolvers": "^2.9.10",
"@notifee/react-native": "^7.8.0",
"@nozbe/watermelondb": "0.23.0",
"@react-native-async-storage/async-storage": "^1.17.11",
"@react-native-camera-roll/camera-roll": "5.6.1",
@ -46,6 +47,7 @@
"@react-native-firebase/analytics": "^14.11.0",
"@react-native-firebase/app": "^14.11.0",
"@react-native-firebase/crashlytics": "^14.11.0",
"@react-native-firebase/messaging": "^18.5.0",
"@react-native-masked-view/masked-view": "^0.2.8",
"@react-navigation/drawer": "6.4.1",
"@react-navigation/elements": "^1.3.6",

View File

@ -0,0 +1,20 @@
--- a/node_modules/@notifee/react-native/android/src/main/java/io/invertase/notifee/NotifeeApiModule.java
+++ b/node_modules/@notifee/react-native/android/src/main/java/io/invertase/notifee/NotifeeApiModule.java
@@ -238,7 +238,7 @@ public class NotifeeApiModule extends ReactContextBaseJavaModule implements Perm
@ReactMethod
public void requestPermission(Promise promise) {
// For Android 12 and below, we return the notification settings
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ if (Build.VERSION.SDK_INT < 33) {
Notifee.getInstance()
.getNotificationSettings(
(e, aBundle) -> NotifeeReactUtils.promiseResolver(promise, e, aBundle));
@@ -265,7 +265,7 @@ public class NotifeeApiModule extends ReactContextBaseJavaModule implements Perm
(e, aBundle) -> NotifeeReactUtils.promiseResolver(promise, e, aBundle));
activity.requestPermissions(
- new String[] {Manifest.permission.POST_NOTIFICATIONS},
+ new String[] {"android.permission.POST_NOTIFICATIONS"},
Notifee.REQUEST_CODE_NOTIFICATION_PERMISSION,
this);
}

View File

@ -14,6 +14,16 @@ module.exports = {
platforms: {
android: null
}
},
'@react-native-firebase/messaging': {
platforms: {
ios: null
}
},
'@notifee/react-native': {
platforms: {
ios: null
}
}
}
};

View File

@ -5153,6 +5153,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@notifee/react-native@^7.8.0":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.8.0.tgz#2990883753990f3585aa0cb5becc5cbdbcd87a43"
integrity sha512-sx8h62U4FrR4pqlbN1rkgPsdamDt9Tad0zgfO6VtP6rNJq/78k8nxUnh0xIX3WPDcCV8KAzdYCE7+UNvhF1CpQ==
"@nozbe/simdjson@0.9.6-fix2":
version "0.9.6-fix2"
resolved "https://registry.yarnpkg.com/@nozbe/simdjson/-/simdjson-0.9.6-fix2.tgz#00d1c8ec76bfac25c022b07511c8fff4568b2973"
@ -5488,6 +5493,11 @@
"@expo/config-plugins" "^4.1.5"
stacktrace-js "^2.0.0"
"@react-native-firebase/messaging@^18.5.0":
version "18.5.0"
resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-18.5.0.tgz#2a80b25816470e9843682e031a3a113566067ce6"
integrity sha512-y1FApYxBMcygmbWBqUPFC+fCfvx6Yf6TdZewun7kPwx+S+tkYzoKx1IsXtxOXtqyJjCNEYirjFgNrs5SSd02zA==
"@react-native-masked-view/masked-view@^0.2.8":
version "0.2.8"
resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.2.8.tgz#34405a4361882dae7c81b1b771fe9f5fbd545a97"