diff --git a/app/managers/network_manager.ts b/app/managers/network_manager.ts index dd82b82fc1d..4d6afb81d72 100644 --- a/app/managers/network_manager.ts +++ b/app/managers/network_manager.ts @@ -11,7 +11,7 @@ import { } from '@mattermost/react-native-network-client'; import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'; import {modelName, osName, osVersion} from 'expo-device'; -import {defineMessages, createIntl} from 'react-intl'; +import {defineMessages} from 'react-intl'; import {Alert, DeviceEventEmitter} from 'react-native'; import urlParse from 'url-parse'; @@ -20,9 +20,9 @@ import {Client} from '@client/rest'; import * as ClientConstants from '@client/rest/constants'; import ClientError from '@client/rest/error'; import {CERTIFICATE_ERRORS} from '@constants/network'; -import {DEFAULT_LOCALE, getTranslations} from '@i18n'; import ManagedApp from '@init/managed_app'; import {toMilliseconds} from '@utils/datetime'; +import {getIntlShape} from '@utils/general'; import {logDebug, logError} from '@utils/log'; import {getCSRFFromCookie} from '@utils/security'; @@ -50,10 +50,7 @@ const messages = defineMessages({ class NetworkManager { private clients: Record = {}; - private intl = createIntl({ - locale: DEFAULT_LOCALE, - messages: getTranslations(DEFAULT_LOCALE), - }); + private intl = getIntlShape(); private DEFAULT_CONFIG: APIClientConfiguration = { headers: { diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index ee1db6498e8..b7ae8c9a772 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {match} from 'path-to-regexp'; -import {createIntl, type IntlShape} from 'react-intl'; +import {type IntlShape} from 'react-intl'; import {Navigation} from 'react-native-navigation'; import urlParse from 'url-parse'; @@ -13,7 +13,7 @@ import DeepLinkType from '@app/constants/deep_linking'; import {DeepLink, Launch, Screens} from '@constants'; import {getDefaultThemeByAppearance} from '@context/theme'; import DatabaseManager from '@database/manager'; -import {DEFAULT_LOCALE, getTranslations, t} from '@i18n'; +import {DEFAULT_LOCALE, t} from '@i18n'; import WebsocketManager from '@managers/websocket_manager'; import {getActiveServerUrl} from '@queries/app/servers'; import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user'; @@ -21,6 +21,7 @@ import {dismissAllModalsAndPopToRoot} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; import {alertErrorWithFallback, errorBadChannel, errorUnkownUser} from '@utils/draft'; +import {getIntlShape} from '@utils/general'; import {logError} from '@utils/log'; import {escapeRegex} from '@utils/markdown'; import {addNewServer} from '@utils/server'; @@ -68,10 +69,7 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, const {database} = DatabaseManager.getServerDatabaseAndOperator(existingServerUrl); const currentUser = await getCurrentUser(database); const locale = currentUser?.locale || DEFAULT_LOCALE; - const intl = intlShape || createIntl({ - locale, - messages: getTranslations(locale), - }); + const intl = intlShape || getIntlShape(locale); switch (parsed.type) { case DeepLink.Channel: { diff --git a/app/utils/errors.test.ts b/app/utils/errors.test.ts index 33ea2075cd2..5ca5b8675d2 100644 --- a/app/utils/errors.test.ts +++ b/app/utils/errors.test.ts @@ -1,10 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {createIntl} from 'react-intl'; - -import {DEFAULT_LOCALE, getTranslations} from '@i18n'; - import { isServerError, isErrorWithMessage, @@ -14,6 +10,7 @@ import { isErrorWithUrl, getFullErrorMessage, } from './errors'; +import {getIntlShape} from './general'; describe('Errors', () => { test('isServerError', () => { @@ -53,8 +50,7 @@ describe('Errors', () => { }); test('getFullErrorMessage', () => { - const locale = DEFAULT_LOCALE; - const intl = createIntl({locale, messages: getTranslations(locale)}); + const intl = getIntlShape(); expect(getFullErrorMessage('error', intl)).toBe('error'); expect(getFullErrorMessage({details: 'more info', message: 'error message'}, intl)).toBe('error message; more info'); expect(getFullErrorMessage({details: 'more info', message: 'error message'}, intl, 3)).toBe('error message; error message'); diff --git a/app/utils/file/file_picker/index.test.ts b/app/utils/file/file_picker/index.test.ts index 3196ca0fd8b..9887f7447b9 100644 --- a/app/utils/file/file_picker/index.test.ts +++ b/app/utils/file/file_picker/index.test.ts @@ -5,16 +5,15 @@ import RNUtils from '@mattermost/rnutils'; import {applicationName} from 'expo-application'; -import {createIntl} from 'react-intl'; import {Alert, Platform} from 'react-native'; import DocumentPicker, {type DocumentPickerResponse} from 'react-native-document-picker'; import {launchCamera, launchImageLibrary, type Asset, type ImagePickerResponse} from 'react-native-image-picker'; import Permissions from 'react-native-permissions'; -import {getTranslations} from '@i18n'; import {dismissBottomSheet} from '@screens/navigation'; import TestHelper from '@test/test_helper'; import {extractFileInfo, lookupMimeType} from '@utils/file'; +import {getIntlShape} from '@utils/general'; import {logWarning} from '@utils/log'; import FilePickerUtil from '.'; @@ -46,7 +45,7 @@ jest.mock('@mattermost/rnutils', () => ({ describe('FilePickerUtil', () => { const mockUploadFiles = jest.fn(); - const intl = createIntl({locale: 'en', messages: getTranslations('en')}); + const intl = getIntlShape(); const originalSelect = Platform.select; let filePickerUtil: FilePickerUtil; diff --git a/app/utils/file/index.test.ts b/app/utils/file/index.test.ts index aa47390272f..2112c089714 100644 --- a/app/utils/file/index.test.ts +++ b/app/utils/file/index.test.ts @@ -2,11 +2,10 @@ // See LICENSE.txt for license information. import {getInfoAsync, deleteAsync} from 'expo-file-system'; -import {createIntl} from 'react-intl'; import {Platform} from 'react-native'; import Permissions from 'react-native-permissions'; -import {getTranslations} from '@i18n'; +import {getIntlShape} from '@utils/general'; import {logError} from '@utils/log'; import {urlSafeBase64Encode} from '@utils/security'; @@ -62,7 +61,7 @@ jest.mock('@utils/mattermost_managed', () => ({ jest.mock('@utils/security', () => ({urlSafeBase64Encode: (url: string) => btoa(url)})); describe('Image utils', () => { - const intl = createIntl({locale: 'en', messages: getTranslations('en')}); + const intl = getIntlShape(); beforeEach(() => { jest.clearAllMocks(); }); diff --git a/app/utils/general/index.test.ts b/app/utils/general/index.test.ts new file mode 100644 index 00000000000..f83737295f5 --- /dev/null +++ b/app/utils/general/index.test.ts @@ -0,0 +1,108 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import ReactNativeHapticFeedback, {HapticFeedbackTypes} from 'react-native-haptic-feedback'; + +import {getIntlShape, emptyFunction, generateId, hapticFeedback, sortByNewest, isBetaApp, type SortByCreatAt} from './'; + +// Mock necessary modules +jest.mock('expo-application', () => ({ + applicationId: 'com.example.rnbeta', +})); + +jest.mock('expo-crypto', () => ({ + randomUUID: jest.fn(() => '12345678-1234-1234-1234-1234567890ab'), +})); + +jest.mock('react-intl', () => ({ + createIntl: jest.fn((config) => config), +})); + +jest.mock('react-native-haptic-feedback', () => ({ + trigger: jest.fn(), + HapticFeedbackTypes: { + impactLight: 'impactLight', + impactMedium: 'impactMedium', + }, +})); + +jest.mock('@i18n', () => ({ + getTranslations: jest.fn((locale) => ({message: `translations for ${locale}`})), + DEFAULT_LOCALE: 'en', +})); + +describe('getIntlShape', () => { + it('should return intl shape with default locale', () => { + const result = getIntlShape(); + expect(result).toEqual({ + locale: 'en', + messages: {message: 'translations for en'}, + }); + }); + + it('should return intl shape with specified locale', () => { + const result = getIntlShape('fr'); + expect(result).toEqual({ + locale: 'fr', + messages: {message: 'translations for fr'}, + }); + }); +}); + +describe('emptyFunction', () => { + it('should do nothing', () => { + expect(emptyFunction()).toBeUndefined(); + }); +}); + +describe('generateId', () => { + it('should generate an ID without prefix', () => { + const result = generateId(); + expect(result).toBe('12345678-1234-1234-1234-1234567890ab'); + }); + + it('should generate an ID with prefix', () => { + const result = generateId('prefix'); + expect(result).toBe('prefix-12345678-1234-1234-1234-1234567890ab'); + }); +}); + +describe('hapticFeedback', () => { + it('should trigger haptic feedback with default method', () => { + hapticFeedback(); + expect(ReactNativeHapticFeedback.trigger).toHaveBeenCalledWith('impactLight', { + enableVibrateFallback: false, + ignoreAndroidSystemSettings: false, + }); + }); + + it('should trigger haptic feedback with specified method', () => { + hapticFeedback(HapticFeedbackTypes.impactMedium); + expect(ReactNativeHapticFeedback.trigger).toHaveBeenCalledWith('impactMedium', { + enableVibrateFallback: false, + ignoreAndroidSystemSettings: false, + }); + }); +}); + +describe('sortByNewest', () => { + it('should sort by newest create_at', () => { + const a = {create_at: 2000} as SortByCreatAt; + const b = {create_at: 1000} as SortByCreatAt; + const result = sortByNewest(a, b); + expect(result).toBe(-1); + }); + + it('should sort by oldest create_at', () => { + const a = {create_at: 1000} as SortByCreatAt; + const b = {create_at: 2000} as SortByCreatAt; + const result = sortByNewest(a, b); + expect(result).toBe(1); + }); +}); + +describe('isBetaApp', () => { + it('should be true if applicationId includes rnbeta', () => { + expect(isBetaApp).toBe(true); + }); +}); diff --git a/app/utils/general/index.ts b/app/utils/general/index.ts index 4b9ea98afb9..5e4b99a2ac2 100644 --- a/app/utils/general/index.ts +++ b/app/utils/general/index.ts @@ -6,13 +6,13 @@ import {randomUUID} from 'expo-crypto'; import {createIntl} from 'react-intl'; import ReactNativeHapticFeedback, {HapticFeedbackTypes} from 'react-native-haptic-feedback'; -import {getTranslations} from '@i18n'; +import {DEFAULT_LOCALE, getTranslations} from '@i18n'; -type SortByCreatAt = (Session | Channel | Team | Post) & { +export type SortByCreatAt = (Session | Channel | Team | Post) & { create_at: number; } -export function getIntlShape(locale = 'en') { +export function getIntlShape(locale = DEFAULT_LOCALE) { return createIntl({ locale, messages: getTranslations(locale), diff --git a/app/utils/notification/index.ts b/app/utils/notification/index.ts index 81309510dc4..275d90d9098 100644 --- a/app/utils/notification/index.ts +++ b/app/utils/notification/index.ts @@ -2,14 +2,15 @@ // See LICENSE.txt for license information. import moment from 'moment-timezone'; -import {createIntl, type IntlShape} from 'react-intl'; +import {type IntlShape} from 'react-intl'; import {Alert, DeviceEventEmitter} from 'react-native'; import {Events} from '@constants'; import {NOTIFICATION_TYPE} from '@constants/push_notification'; -import {DEFAULT_LOCALE, getTranslations} from '@i18n'; +import {DEFAULT_LOCALE} from '@i18n'; import PushNotifications from '@init/push_notifications'; import {popToRoot} from '@screens/navigation'; +import {getIntlShape} from '@utils/general'; export const convertToNotificationData = (notification: Notification, tapped = true): NotificationWithData => { if (!notification.payload) { @@ -95,7 +96,7 @@ export const scheduleExpiredNotification = (serverUrl: string, session: Session, const expiresInHours = Math.ceil(Math.abs(moment.duration(moment().diff(moment(expiresAt))).asHours())); const expiresInDays = Math.floor(expiresInHours / 24); // Calculate expiresInDays const remainingHours = expiresInHours % 24; // Calculate remaining hours - const intl = createIntl({locale, messages: getTranslations(locale)}); + const intl = getIntlShape(locale); let body = ''; if (expiresInDays === 0) { body = intl.formatMessage({