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:
parent
501e42196d
commit
e06aaf1cc0
|
@ -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']);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -794,6 +794,8 @@
|
|||
"You_dont_have_permission_to_perform_this_action": "You don’t 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",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export const NOTIFICATION_PRESENCE_CAP = 'NOTIFICATION_PRESENCE_CAP';
|
||||
export const NOTIFICATION_IN_APP_VIBRATION = 'NOTIFICATION_IN_APP_VIBRATION';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -31,8 +31,8 @@ import RoomInfoViewTitle from './components/RoomInfoViewTitle';
|
|||
import styles from './styles';
|
||||
|
||||
type TRoomInfoViewNavigationProp = CompositeNavigationProp<
|
||||
StackNavigationProp<ChatsStackParamList, 'RoomInfoView'>,
|
||||
StackNavigationProp<MasterDetailInsideStackParamList>
|
||||
StackNavigationProp<ChatsStackParamList, 'RoomInfoView'>,
|
||||
StackNavigationProp<MasterDetailInsideStackParamList>
|
||||
>;
|
||||
|
||||
type TRoomInfoViewRouteProp = RouteProp<ChatsStackParamList, 'RoomInfoView'>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)))));
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue