Skip to content

Commit

Permalink
feat (Android): mobile ringer (#5286)
Browse files Browse the repository at this point in the history
  • Loading branch information
dnlsilva authored Nov 24, 2023
1 parent bd17ee5 commit 31ed940
Show file tree
Hide file tree
Showing 21 changed files with 335 additions and 36 deletions.
Binary file added android/app/src/main/res/raw/ringtone.mp3
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,13 @@ public void call(@Nullable Bundle bundle) {
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
Expand Down
2 changes: 1 addition & 1 deletion app/actions/actionsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down
7 changes: 7 additions & 0 deletions app/actions/deepLinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
2 changes: 0 additions & 2 deletions app/containers/Ringer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
4 changes: 3 additions & 1 deletion app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
4 changes: 2 additions & 2 deletions app/i18n/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
25 changes: 14 additions & 11 deletions app/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';

Expand Down Expand Up @@ -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!);
Expand Down
2 changes: 2 additions & 0 deletions app/lib/constants/colors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const BACKGROUND_PUSH_COLOR = '#F5455C';

export const STATUS_COLORS: any = {
online: '#2de0a5',
busy: '#f5455c',
Expand Down
2 changes: 2 additions & 0 deletions app/lib/methods/helpers/goRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,5 @@ export const goRoom = async ({

return navigate({ item, isMasterDetail, popToRoot, ...props });
};

export const navigateToRoom = navigate;
8 changes: 6 additions & 2 deletions app/lib/methods/videoConf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
};
6 changes: 3 additions & 3 deletions app/lib/notifications/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
122 changes: 122 additions & 0 deletions app/lib/notifications/videoConf/backgroundNotificationHandler.ts
Original file line number Diff line number Diff line change
@@ -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();
15 changes: 15 additions & 0 deletions app/lib/notifications/videoConf/getInitialNotification.ts
Original file line number Diff line number Diff line change
@@ -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 })
);
}
}
};
Loading

0 comments on commit 31ed940

Please sign in to comment.