diff --git a/app/actions/actionsTypes.ts b/app/actions/actionsTypes.ts index 2273e3363..cad9c6c04 100644 --- a/app/actions/actionsTypes.ts +++ b/app/actions/actionsTypes.ts @@ -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']); diff --git a/app/actions/inAppFeedback.ts b/app/actions/inAppFeedback.ts new file mode 100644 index 000000000..91e8dbffb --- /dev/null +++ b/app/actions/inAppFeedback.ts @@ -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 + }; +} diff --git a/app/containers/InAppNotification/index.tsx b/app/containers/InAppNotification/index.tsx index 8337f8f3c..9c45052e9 100644 --- a/app/containers/InAppNotification/index.tsx +++ b/app/containers/InAppNotification/index.tsx @@ -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), diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts index cb7d8eaa1..5724c49b9 100644 --- a/app/definitions/redux/index.ts +++ b/app/definitions/redux/index.ts @@ -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; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index a5f572dc2..a9ff444ed 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -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", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 5d90a3efd..cb4f4d7d2 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -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" } \ No newline at end of file diff --git a/app/lib/constants/notifications.ts b/app/lib/constants/notifications.ts index 2b3b08185..e59a5b71a 100644 --- a/app/lib/constants/notifications.ts +++ b/app/lib/constants/notifications.ts @@ -1 +1,2 @@ export const NOTIFICATION_PRESENCE_CAP = 'NOTIFICATION_PRESENCE_CAP'; +export const NOTIFICATION_IN_APP_VIBRATION = 'NOTIFICATION_IN_APP_VIBRATION'; diff --git a/app/lib/methods/userPreferences.ts b/app/lib/methods/userPreferences.ts index f9b82dec4..f61944bd7 100644 --- a/app/lib/methods/userPreferences.ts +++ b/app/lib/methods/userPreferences.ts @@ -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; diff --git a/app/reducers/inAppFeedback.test.ts b/app/reducers/inAppFeedback.test.ts new file mode 100644 index 000000000..50163deb7 --- /dev/null +++ b/app/reducers/inAppFeedback.test.ts @@ -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); + }); +}); diff --git a/app/reducers/inAppFeedback.ts b/app/reducers/inAppFeedback.ts new file mode 100644 index 000000000..0f1a86718 --- /dev/null +++ b/app/reducers/inAppFeedback.ts @@ -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; + } +} diff --git a/app/reducers/index.js b/app/reducers/index.js index d3f743a58..cdc02f392 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -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 }); diff --git a/app/views/ChangeAvatarView/index.tsx b/app/views/ChangeAvatarView/index.tsx index 7339d61e2..9f8c65d54 100644 --- a/app/views/ChangeAvatarView/index.tsx +++ b/app/views/ChangeAvatarView/index.tsx @@ -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); } } diff --git a/app/views/DirectoryView/index.tsx b/app/views/DirectoryView/index.tsx index b8f17d234..ee5f9a357 100644 --- a/app/views/DirectoryView/index.tsx +++ b/app/views/DirectoryView/index.tsx @@ -139,7 +139,7 @@ class DirectoryView extends React.Component { diff --git a/app/views/RoomInfoView/components/RoomInfoButtons.tsx b/app/views/RoomInfoView/components/RoomInfoButtons.tsx index 8f7c36d20..74be791c4 100644 --- a/app/views/RoomInfoView/components/RoomInfoButtons.tsx +++ b/app/views/RoomInfoView/components/RoomInfoButtons.tsx @@ -111,13 +111,7 @@ export const RoomInfoButtons = ({ iconName='ignore' showIcon={!!renderBlockUser} /> - + ); }; diff --git a/app/views/RoomInfoView/index.tsx b/app/views/RoomInfoView/index.tsx index 4ecf3c0e7..dc0e31ea7 100644 --- a/app/views/RoomInfoView/index.tsx +++ b/app/views/RoomInfoView/index.tsx @@ -31,8 +31,8 @@ import RoomInfoViewTitle from './components/RoomInfoViewTitle'; import styles from './styles'; type TRoomInfoViewNavigationProp = CompositeNavigationProp< -StackNavigationProp, -StackNavigationProp + StackNavigationProp, + StackNavigationProp >; type TRoomInfoViewRouteProp = RouteProp; diff --git a/app/views/RoomView/definitions.ts b/app/views/RoomView/definitions.ts index 5c246ff71..8feadc3d8 100644 --- a/app/views/RoomView/definitions.ts +++ b/app/views/RoomView/definitions.ts @@ -22,6 +22,7 @@ export interface IRoomViewProps extends IActionSheetProvider, IBaseScreen { } 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 { }; 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 { 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 { }; content = ; } else { + if (inAppFeedback?.[item.id]) { + this.hapticFeedback(item.id); + } content = ( ({ 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))))); diff --git a/app/views/UserNotificationPreferencesView/index.tsx b/app/views/UserNotificationPreferencesView/index.tsx index 1dcfc5fe4..739756440 100644 --- a/app/views/UserNotificationPreferencesView/index.tsx +++ b/app/views/UserNotificationPreferencesView/index.tsx @@ -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(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 ( @@ -92,6 +100,18 @@ const UserNotificationPreferencesView = () => { + + + ( + + )} + /> + + +