feat: haptics feedback in app notificaiton (#5507)

* create redux for inAppFeedback

* add the clear inAppFeedback, haptic feedback to room view

* added haptics feedback to room view

* add the user preference to in app vibration and the value

* minor tweak

* yarn prettier-lint
This commit is contained in:
Reinaldo Neto 2024-01-29 12:54:37 -03:00 committed by GitHub
parent 501e42196d
commit e06aaf1cc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 184 additions and 21 deletions

View File

@ -97,3 +97,4 @@ export const VIDEO_CONF = createRequestTypes('VIDEO_CONF', [
'SET_CALLING'
]);
export const SUPPORTED_VERSIONS = createRequestTypes('SUPPORTED_VERSIONS', ['SET']);
export const IN_APP_FEEDBACK = createRequestTypes('IN_APP_FEEDBACK', ['SET', 'REMOVE', 'CLEAR']);

View File

@ -0,0 +1,29 @@
import { Action } from 'redux';
import { IN_APP_FEEDBACK } from './actionsTypes';
interface IInAppFeedbackAction {
msgId: string;
}
export type TInAppFeedbackAction = IInAppFeedbackAction & Action;
export function setInAppFeedback(msgId: string): TInAppFeedbackAction {
return {
type: IN_APP_FEEDBACK.SET,
msgId
};
}
export function removeInAppFeedback(msgId: string): TInAppFeedbackAction {
return {
type: IN_APP_FEEDBACK.REMOVE,
msgId
};
}
export function clearInAppFeedback(): Action {
return {
type: IN_APP_FEEDBACK.CLEAR
};
}

View File

@ -1,11 +1,13 @@
import React, { ElementType, memo, useEffect } from 'react';
import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
import { useDispatch } from 'react-redux';
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
import EventEmitter from '../../lib/methods/helpers/events';
import Navigation from '../../lib/navigation/appNavigation';
import { getActiveRoute } from '../../lib/methods/helpers/navigation';
import { useAppSelector } from '../../lib/hooks';
import { setInAppFeedback } from '../../actions/inAppFeedback';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
@ -15,6 +17,8 @@ const InAppNotification = memo(() => {
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
}));
const dispatch = useDispatch();
const show = (
notification: INotifierComponent['notification'] & {
customComponent?: ElementType;
@ -30,7 +34,13 @@ const InAppNotification = memo(() => {
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
if (payload?.rid || notification.customNotification) {
if (payload?.rid === subscribedRoom || route?.name === 'JitsiMeetView' || payload?.message?.t === 'videoconf') return;
if (route?.name === 'JitsiMeetView' || payload?.message?.t === 'videoconf') return;
if (payload?.rid === subscribedRoom) {
const msgId = payload._id;
dispatch(setInAppFeedback(msgId));
return;
}
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),

View File

@ -18,6 +18,7 @@ import { TActionPermissions } from '../../actions/permissions';
import { TActionEnterpriseModules } from '../../actions/enterpriseModules';
import { TActionVideoConf } from '../../actions/videoConf';
import { TActionSupportedVersions } from '../../actions/supportedVersions';
import { TInAppFeedbackAction } from '../../actions/inAppFeedback';
// REDUCERS
import { IActiveUsers } from '../../reducers/activeUsers';
import { IApp } from '../../reducers/app';
@ -40,6 +41,7 @@ import { IVideoConf } from '../../reducers/videoConf';
import { TActionUsersRoles } from '../../actions/usersRoles';
import { TUsersRoles } from '../../reducers/usersRoles';
import { ISupportedVersionsState } from '../../reducers/supportedVersions';
import { IInAppFeedbackState } from '../../reducers/inAppFeedback';
export interface IApplicationState {
settings: TSettingsState;
@ -66,6 +68,7 @@ export interface IApplicationState {
videoConf: IVideoConf;
usersRoles: TUsersRoles;
supportedVersions: ISupportedVersionsState;
inAppFeedback: IInAppFeedbackState;
}
export type TApplicationActions = TActionActiveUsers &
@ -87,4 +90,5 @@ export type TApplicationActions = TActionActiveUsers &
TActionEnterpriseModules &
TActionVideoConf &
TActionUsersRoles &
TActionSupportedVersions;
TActionSupportedVersions &
TInAppFeedbackAction;

View File

@ -794,6 +794,8 @@
"You_dont_have_permission_to_perform_this_action": "You dont have permission to perform this action. Check with a workspace administrator.",
"Jump_to_message": "Jump to message",
"Missed_call": "Missed call",
"In_app_message_notifications": "In app message notifications",
"Vibrate": "Vibrate",
"Recording_audio_in_progress": "Recording audio message",
"Bold": "Bold",
"Italic": "Italic",

View File

@ -793,5 +793,7 @@
"Pinned_a_message": "Fixou uma mensagem:",
"You_dont_have_permission_to_perform_this_action": "Você não tem permissão para realizar esta ação. Verifique com um administrador do espaço de trabalho.",
"Jump_to_message": "Ir para mensagem",
"Missed_call": "Chamada perdida"
"Missed_call": "Chamada perdida",
"In_app_message_notifications": "Notificações de mensagens in-app",
"Vibrate": "Vibrar"
}

View File

@ -1 +1,2 @@
export const NOTIFICATION_PRESENCE_CAP = 'NOTIFICATION_PRESENCE_CAP';
export const NOTIFICATION_IN_APP_VIBRATION = 'NOTIFICATION_IN_APP_VIBRATION';

View File

@ -29,7 +29,6 @@ class UserPreferences {
getBool(key: string): boolean | null {
try {
console.log(this.mmkv.getBool(key));
return this.mmkv.getBool(key) ?? null;
} catch {
return null;

View File

@ -0,0 +1,33 @@
import { removeInAppFeedback, setInAppFeedback, clearInAppFeedback } from '../actions/inAppFeedback';
import { mockedStore } from './mockedStore';
import { initialState } from './inAppFeedback';
describe('test inAppFeedback reducer', () => {
it('should return initial state', () => {
const state = mockedStore.getState().inAppFeedback;
expect(state).toEqual(initialState);
});
const msgId01 = 'msgId01';
const msgId02 = 'msgId02';
it('should return modified store after setInAppFeedback', () => {
const resultExpected = { [msgId01]: msgId01, [msgId02]: msgId02 };
mockedStore.dispatch(setInAppFeedback(msgId01));
mockedStore.dispatch(setInAppFeedback(msgId02));
const state = mockedStore.getState().inAppFeedback;
expect(state).toEqual(resultExpected);
});
it('should return modified store after removeInAppFeedback', () => {
const resultExpected = { [msgId02]: msgId02 };
mockedStore.dispatch(removeInAppFeedback(msgId01));
const state = mockedStore.getState().inAppFeedback;
expect(state).toEqual(resultExpected);
});
it('should return empty store after clearInAppFeedback', () => {
mockedStore.dispatch(clearInAppFeedback());
const state = mockedStore.getState().inAppFeedback;
expect(state).toEqual(initialState);
});
});

View File

@ -0,0 +1,27 @@
import { IN_APP_FEEDBACK } from '../actions/actionsTypes';
import { TApplicationActions } from '../definitions';
export interface IInAppFeedbackState {
[key: string]: string;
}
export const initialState: IInAppFeedbackState = {};
export default function activeUsers(state = initialState, action: TApplicationActions): IInAppFeedbackState {
switch (action.type) {
case IN_APP_FEEDBACK.SET:
const { msgId } = action;
return {
...state,
[msgId]: msgId
};
case IN_APP_FEEDBACK.REMOVE:
const newState = { ...state };
delete newState[action.msgId];
return newState;
case IN_APP_FEEDBACK.CLEAR:
return initialState;
default:
return state;
}
}

View File

@ -24,6 +24,7 @@ import roles from './roles';
import videoConf from './videoConf';
import usersRoles from './usersRoles';
import supportedVersions from './supportedVersions';
import inAppFeedback from './inAppFeedback';
export default combineReducers({
settings,
@ -49,5 +50,6 @@ export default combineReducers({
roles,
videoConf,
usersRoles,
supportedVersions
supportedVersions,
inAppFeedback
});

View File

@ -147,7 +147,7 @@ const ChangeAvatarView = () => {
payload: { url: response.path, data: `data:image/jpeg;base64,${response.data}`, service: 'upload' }
});
} catch (error: any) {
if(error?.code !== "E_PICKER_CANCELLED") {
if (error?.code !== 'E_PICKER_CANCELLED') {
log(error);
}
}

View File

@ -139,7 +139,7 @@ class DirectoryView extends React.Component<IDirectoryViewProps, IDirectoryViewS
} else if (type === 'teams') {
logEvent(events.DIRECTORY_SEARCH_TEAMS);
}
this.toggleDropdown()
this.toggleDropdown();
};
toggleWorkspace = () => {

View File

@ -111,13 +111,7 @@ export const RoomInfoButtons = ({
iconName='ignore'
showIcon={!!renderBlockUser}
/>
<BaseButton
onPress={handleReportUser}
label={i18n.t('Report')}
iconName='warning'
showIcon={!!renderReportUser}
danger
/>
<BaseButton onPress={handleReportUser} label={i18n.t('Report')} iconName='warning' showIcon={!!renderReportUser} danger />
</View>
);
};

View File

@ -22,6 +22,7 @@ export interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsS
transferLivechatGuestPermission?: string[]; // TODO: Check if its the correct type
viewCannedResponsesPermission?: string[]; // TODO: Check if its the correct type
livechatAllowManualOnHold?: boolean;
inAppFeedback?: { [key: string]: string };
}
export type TStateAttrsUpdate = keyof IRoomViewState;

View File

@ -7,6 +7,7 @@ import { Q } from '@nozbe/watermelondb';
import { dequal } from 'dequal';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { Subscription } from 'rxjs';
import * as Haptics from 'expo-haptics';
import { getRoutingConfig } from '../../lib/services/restApi';
import Touch from '../../containers/Touch';
@ -62,7 +63,14 @@ import {
TGetCustomEmoji,
RoomType
} from '../../definitions';
import { E2E_MESSAGE_TYPE, E2E_STATUS, MESSAGE_TYPE_ANY_LOAD, MessageTypeLoad, themes } from '../../lib/constants';
import {
E2E_MESSAGE_TYPE,
E2E_STATUS,
MESSAGE_TYPE_ANY_LOAD,
MessageTypeLoad,
themes,
NOTIFICATION_IN_APP_VIBRATION
} from '../../lib/constants';
import { ModalStackParamList } from '../../stacks/MasterDetailStack/types';
import {
callJitsi,
@ -90,6 +98,8 @@ import audioPlayer from '../../lib/methods/audioPlayer';
import { IListContainerRef, TListRef } from './List/definitions';
import { getMessageById } from '../../lib/database/services/Message';
import { getThreadById } from '../../lib/database/services/Thread';
import { clearInAppFeedback, removeInAppFeedback } from '../../actions/inAppFeedback';
import UserPreferences from '../../lib/methods/userPreferences';
import { IRoomViewProps, IRoomViewState } from './definitions';
import { roomAttrsUpdate, stateAttrsUpdate } from './constants';
@ -197,8 +207,9 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
}
componentDidMount() {
const { navigation, dispatch } = this.props;
const { selectedMessages } = this.state;
const { navigation } = this.props;
dispatch(clearInAppFeedback());
this.mounted = true;
this.didMountInteraction = InteractionManager.runAfterInteractions(() => {
const { isAuthenticated } = this.props;
@ -302,6 +313,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
};
async componentWillUnmount() {
const { dispatch } = this.props;
dispatch(clearInAppFeedback());
this.mounted = false;
this.unsubscribe();
if (this.didMountInteraction && this.didMountInteraction.cancel) {
@ -1224,10 +1237,31 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
Navigation.navigate('CannedResponsesListView', { rid: room.rid });
};
hapticFeedback = (msgId: string) => {
const { dispatch } = this.props;
dispatch(removeInAppFeedback(msgId));
const notificationInAppVibration = UserPreferences.getBool(NOTIFICATION_IN_APP_VIBRATION);
if (notificationInAppVibration || notificationInAppVibration === null) {
try {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch {
// Do nothing: Haptic is unavailable
}
}
};
renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => {
const { room, lastOpen, canAutoTranslate } = this.state;
const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme } =
this.props;
const {
user,
Message_GroupingPeriod,
Message_TimeFormat,
useRealName,
baseUrl,
Message_Read_Receipt_Enabled,
theme,
inAppFeedback
} = this.props;
const { action, selectedMessages } = this.state;
let dateSeparator = null;
let showUnreadSeparator = false;
@ -1254,6 +1288,9 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
};
content = <LoadMore rid={room.rid} t={room.t as RoomType} loaderId={item.id} type={item.t} runOnRender={runOnRender()} />;
} else {
if (inAppFeedback?.[item.id]) {
this.hapticFeedback(item.id);
}
content = (
<Message
item={item}
@ -1458,7 +1495,8 @@ const mapStateToProps = (state: IApplicationState) => ({
Hide_System_Messages: state.settings.Hide_System_Messages as string[],
transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'],
viewCannedResponsesPermission: state.permissions['view-canned-responses'],
livechatAllowManualOnHold: state.settings.Livechat_allow_manual_on_hold as boolean
livechatAllowManualOnHold: state.settings.Livechat_allow_manual_on_hold as boolean,
inAppFeedback: state.inAppFeedback
});
export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(withActionSheet(RoomView)))));

View File

@ -1,4 +1,5 @@
import React, { useEffect, useLayoutEffect, useState } from 'react';
import { Switch } from 'react-native';
import { StackNavigationProp } from '@react-navigation/stack';
import { useNavigation } from '@react-navigation/native';
@ -14,8 +15,11 @@ import { Services } from '../../lib/services';
import { useAppSelector } from '../../lib/hooks';
import ListPicker from './ListPicker';
import log from '../../lib/methods/helpers/log';
import { useUserPreferences } from '../../lib/methods';
import { NOTIFICATION_IN_APP_VIBRATION, SWITCH_TRACK_COLOR } from '../../lib/constants';
const UserNotificationPreferencesView = () => {
const [inAppVibration, setInAppVibration] = useUserPreferences<boolean>(NOTIFICATION_IN_APP_VIBRATION, true);
const [preferences, setPreferences] = useState({} as INotificationPreferences);
const [loading, setLoading] = useState(true);
@ -58,6 +62,10 @@ const UserNotificationPreferencesView = () => {
}
};
const toggleInAppVibration = () => {
setInAppVibration(!inAppVibration);
};
return (
<SafeAreaView testID='user-notification-preference-view'>
<StatusBar />
@ -92,6 +100,18 @@ const UserNotificationPreferencesView = () => {
<List.Info info='Push_Notifications_Alert_Info' />
</List.Section>
<List.Section title='In_app_message_notifications'>
<List.Separator />
<List.Item
title='Vibrate'
testID='user-notification-preference-view-in-app-vibration'
right={() => (
<Switch value={inAppVibration} trackColor={SWITCH_TRACK_COLOR} onValueChange={toggleInAppVibration} />
)}
/>
<List.Separator />
</List.Section>
<List.Section title='Email'>
<List.Separator />
<ListPicker