From 0c4a42a06a4008e23a8ed4503265d4bf969f7887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 25 Aug 2025 12:03:01 +0200 Subject: [PATCH] Remove tand prevent double tap (#9078) * Remove the t function and all wrong uses of preventDoubleTap * Fix existing typo * Fix tests * Address comments * Fix test --- app/actions/local/thread.ts | 18 ++- app/actions/remote/apps.ts | 4 +- app/actions/remote/command.test.ts | 2 +- app/actions/remote/command.ts | 2 +- app/actions/remote/general.ts | 15 ++- app/client/rest/tracking.ts | 14 +- app/components/app_version/index.tsx | 12 +- .../autocomplete/at_mention/utils.ts | 74 +++++++---- .../channel_mention/channel_mention.tsx | 42 ++++-- .../app_command_parser/app_command_parser.ts | 8 +- .../autocomplete_selector/index.tsx | 12 +- .../leave_channel_label.tsx | 95 +++++++++----- .../manage_members_label.tsx | 2 +- .../channel_bookmarks/add_bookmark.tsx | 4 +- .../common_post_options/base_option/index.tsx | 10 +- .../copy_permalink_option.tsx | 15 ++- .../follow_thread_option.tsx | 40 ++++-- .../common_post_options/reply_option.tsx | 12 +- .../common_post_options/save_option.tsx | 23 +++- app/components/copy_text_option/index.tsx | 15 ++- app/components/custom_status/clear_button.tsx | 6 +- app/components/files/file_filter.tsx | 62 ++++++--- .../channel_mention/channel_mention.tsx | 17 ++- .../markdown/markdown_code_block/index.tsx | 6 +- .../markdown/markdown_latex_block/index.tsx | 8 +- app/components/no_results_with_term/index.tsx | 26 ++-- app/components/post_draft/archived/index.tsx | 23 ++-- .../post_draft/post_input/post_input.tsx | 13 +- .../combined_user_activity/messages.ts | 119 ++++++++--------- .../post_list/post/avatar/avatar.tsx | 8 +- .../post/body/add_members/add_members.tsx | 4 +- .../button_binding/index.tsx | 6 +- .../action_button/index.tsx | 6 +- .../post/body/reactions/reactions.tsx | 12 +- .../post_list/post/pre_header/index.tsx | 32 +++-- .../post/system_message/system_message.tsx | 123 +++++++++++------- .../thread_overview/thread_overview.tsx | 12 +- app/components/slide_up_panel_item/index.tsx | 6 +- app/components/status_label/index.tsx | 42 ++++-- .../team_sidebar/add_team/index.tsx | 6 +- app/components/touchable_emoji/index.tsx | 4 +- .../touchable_emoji/skinned_emoji.tsx | 6 +- app/components/user_list_row/index.tsx | 26 ++-- app/constants/custom_status.ts | 20 +-- app/context/extra_keyboard/index.tsx | 6 +- app/helpers/api/channel.ts | 2 +- app/i18n/index.ts | 4 - app/init/push_notifications.ts | 24 +++- app/managers/global_event_handler.ts | 24 +++- app/managers/security_manager/index.test.ts | 20 +-- app/managers/security_manager/index.ts | 4 + app/products/calls/actions/calls.test.ts | 2 +- app/products/calls/actions/calls.ts | 6 +- app/products/calls/alerts.test.ts | 4 +- app/products/calls/alerts.ts | 4 +- .../components/audio_device_button.ios.tsx | 4 +- .../calls/components/audio_device_button.tsx | 4 +- .../channel_info_enable_calls/index.tsx | 6 +- .../channel_info_start_button.tsx | 21 ++- app/products/calls/components/messages.tsx | 11 ++ app/screens/apps_form/apps_form_component.tsx | 6 +- app/screens/apps_form/index.tsx | 2 +- .../public_or_private_channel.tsx | 51 +++++--- .../channel_add_members.tsx | 7 +- app/screens/channel_files/header.tsx | 11 +- .../destructive_options/archive/archive.tsx | 108 +++++++++------ .../convert_private/convert_private.tsx | 107 +++++++++------ .../options/add_members/add_members.tsx | 8 +- .../auto_follow_threads.tsx | 8 +- .../options/channel_files/channel_files.tsx | 8 +- .../options/edit_channel/index.tsx | 8 +- .../ignore_mentions/ignore_mentions.tsx | 8 +- .../channel_info/options/members/members.tsx | 8 +- .../notification_preference.tsx | 47 ++++--- .../pinned_messages/pinned_messages.tsx | 8 +- .../notify_about.tsx | 33 +++-- .../thread_replies.tsx | 18 ++- .../convert_gm_to_channel_form.tsx | 6 +- .../channel_info_form.tsx | 19 ++- .../custom_status_suggestion.tsx | 8 +- .../components/custom_status_suggestions.tsx | 44 +++++-- .../components/recent_custom_statuses.tsx | 3 +- app/screens/custom_status/custom_status.tsx | 17 ++- .../components/clear_after_menu_item.tsx | 6 +- app/screens/edit_profile/components/form.tsx | 19 ++- .../components/profile_image_picker.tsx | 6 +- app/screens/edit_profile/edit_profile.tsx | 51 +++++--- .../picker/emoji_category_bar/icon.tsx | 4 +- .../unfiltered_list/unfiltered_list.tsx | 13 +- app/screens/forgot_password/index.tsx | 14 +- .../global_threads/global_threads.test.tsx | 2 +- app/screens/global_threads/global_threads.tsx | 2 +- .../threads_list/thread/thread.tsx | 8 +- .../options/custom_status/index.tsx | 10 +- .../components/options/logout/index.tsx | 2 +- .../options/user_presence/index.tsx | 56 ++++---- .../categories/unreads/unreads.tsx | 2 +- .../subheader/search_field/index.tsx | 6 +- .../home/search/initial/modifiers/index.tsx | 8 +- app/screens/in_app_notification/index.tsx | 12 +- .../integration_selector.tsx | 24 ++-- app/screens/invite/selection_team_bar.tsx | 8 +- app/screens/login/form.tsx | 116 ++++++++--------- app/screens/login/index.tsx | 6 +- .../options/delete_post_option.tsx | 16 ++- .../options/edit_option/edit_option.tsx | 14 +- .../mark_unread_option/mark_unread_option.tsx | 14 +- .../options/pin_channel_option.tsx | 25 ++-- app/screens/post_priority_picker/utils.ts | 36 ++--- .../reschedule_draft/reschedule_draft.tsx | 2 +- app/screens/server/index.tsx | 9 +- app/screens/settings/about/about.tsx | 19 ++- app/screens/settings/about/learn_more.tsx | 22 +++- app/screens/settings/about/subtitle.tsx | 22 +++- app/screens/settings/about/title.tsx | 30 +++-- app/screens/settings/about/tos_privacy.tsx | 5 +- app/screens/settings/advanced/advanced.tsx | 4 +- app/screens/settings/config.ts | 2 +- app/screens/settings/display/display.tsx | 57 ++++---- .../settings/display_crt/display_crt.tsx | 9 +- .../notification_auto_responder.tsx | 15 +-- .../notification_mention/mention_settings.tsx | 9 +- .../notification_mention/reply_settings.tsx | 9 +- .../settings/notification_push/push_send.tsx | 9 +- .../settings/notifications/notifications.tsx | 6 +- app/screens/settings/settings.tsx | 24 ++-- .../thread_follow_button.tsx | 31 +++-- .../options/mark_as_unread_option.tsx | 20 ++- .../options/open_in_channel_option.tsx | 16 ++- app/screens/thread_options/thread_options.tsx | 2 +- app/screens/user_profile/options.tsx | 2 +- app/utils/deep_link/index.test.ts | 3 - app/utils/deep_link/index.ts | 10 +- app/utils/draft/index.ts | 59 +++++---- app/utils/emoji/index.ts | 83 +++++++++++- app/utils/error_handling.ts | 24 +++- app/utils/navigation/index.ts | 2 +- app/utils/post/index.test.ts | 53 ++------ app/utils/post/index.ts | 2 +- app/utils/tap/{test.ts => index.test.ts} | 0 app/utils/user/index.ts | 60 ++++++--- assets/base/i18n/en.json | 21 ++- 142 files changed, 1716 insertions(+), 1106 deletions(-) create mode 100644 app/products/calls/components/messages.tsx rename app/utils/tap/{test.ts => index.test.ts} (100%) diff --git a/app/actions/local/thread.ts b/app/actions/local/thread.ts index f465d3f8a..6fcfe01a0 100644 --- a/app/actions/local/thread.ts +++ b/app/actions/local/thread.ts @@ -1,11 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {defineMessages} from 'react-intl'; import {DeviceEventEmitter} from 'react-native'; import {ActionType, General, Navigation, Screens} from '@constants'; import DatabaseManager from '@database/manager'; -import {getTranslations, t} from '@i18n'; +import {getTranslations} from '@i18n'; import {getChannelById} from '@queries/servers/channel'; import {getPostById} from '@queries/servers/post'; import {getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, type PrepareCommonSystemValuesArgs, setCurrentTeamAndChannelId} from '@queries/servers/system'; @@ -57,6 +58,17 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string, } }; +const threadMessages = defineMessages({ + thread: { + id: 'thread.header.thread', + defaultMessage: 'Thread', + }, + threadIn: { + id: 'thread.header.thread_in', + defaultMessage: 'in {channelName}', + }, +}); + export const switchToThread = async (serverUrl: string, rootId: string, isFromNotification = false) => { try { const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); @@ -112,14 +124,14 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo const translations = getTranslations(user.locale); // Get title translation or default title message - const title = translations[t('thread.header.thread')] || 'Thread'; + const title = translations[threadMessages.thread.id] || 'Thread'; let subtitle = ''; if (channel?.type === General.DM_CHANNEL) { subtitle = channel.displayName; } else { // Get translation or default message - subtitle = translations[t('thread.header.thread_in')] || 'in {channelName}'; + subtitle = translations[threadMessages.threadIn.id] || 'in {channelName}'; subtitle = subtitle.replace('{channelName}', channel.displayName); } diff --git a/app/actions/remote/apps.ts b/app/actions/remote/apps.ts index 167eaa87d..87ef93d71 100644 --- a/app/actions/remote/apps.ts +++ b/app/actions/remote/apps.ts @@ -81,7 +81,7 @@ export async function doAppSubmit(serverUrl: string, inCall: AppCal if (!res.form?.submit) { const errMsg = intl.formatMessage({ id: 'apps.error.responses.form.no_form', - defaultMessage: 'Response type is `form`, but no valid form was included in response.', + defaultMessage: 'Response type is `form`, but no valid form was included in the response.', }); return {error: makeCallErrorResponse(errMsg)}; } @@ -133,7 +133,7 @@ export async function doAppFetchForm(serverUrl: string, call: AppCa if (!res.form?.submit) { const errMsg = intl.formatMessage({ id: 'apps.error.responses.form.no_form', - defaultMessage: 'Response type is `form`, but no valid form was included in response.', + defaultMessage: 'Response type is `form`, but no valid form was included in the response.', }); return {error: makeCallErrorResponse(errMsg)}; } diff --git a/app/actions/remote/command.test.ts b/app/actions/remote/command.test.ts index 858ef7990..2f18be4e5 100644 --- a/app/actions/remote/command.test.ts +++ b/app/actions/remote/command.test.ts @@ -282,7 +282,7 @@ describe('executeAppCommand', () => { expect(parser.composeCommandSubmitCall).toHaveBeenCalledWith(msg); expect(doAppSubmit).toHaveBeenCalledWith(serverUrl, {}, intl); - expect(result).toEqual({error: {message: 'Unknown error.'}}); + expect(result).toEqual({error: {message: 'Unknown error occurred.'}}); }); it('should handle a form response', async () => { diff --git a/app/actions/remote/command.ts b/app/actions/remote/command.ts index f907955ba..a2b6effbf 100644 --- a/app/actions/remote/command.ts +++ b/app/actions/remote/command.ts @@ -97,7 +97,7 @@ export const executeAppCommand = async (serverUrl: string, intl: IntlShape, pars const errorResponse = res.error; return createErrorMessage(errorResponse.text || intl.formatMessage({ id: 'apps.error.unknown', - defaultMessage: 'Unknown error.', + defaultMessage: 'Unknown error occurred.', })); } const callResp = res.data; diff --git a/app/actions/remote/general.ts b/app/actions/remote/general.ts index d076eeec5..92ee79ebe 100644 --- a/app/actions/remote/general.ts +++ b/app/actions/remote/general.ts @@ -1,10 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {defineMessage} from 'react-intl'; + import {SYSTEM_IDENTIFIERS} from '@constants/database'; import {PUSH_PROXY_RESPONSE_VERIFIED, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; import DatabaseManager from '@database/manager'; -import {t} from '@i18n'; import NetworkManager from '@managers/network_manager'; import {getDeviceToken} from '@queries/app/global'; import {getExpandedLinks, getPushVerificationStatus} from '@queries/servers/system'; @@ -41,15 +42,15 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou return {error}; } - const certificateError = { - id: t('mobile.server_requires_client_certificate'), + const certificateError = defineMessage({ + id: 'mobile.server_requires_client_certificate', defaultMessage: 'Server requires client certificate for authentication.', - }; + }); - const pingError = { - id: t('mobile.server_ping_failed'), + const pingError = defineMessage({ + id: 'mobile.server_ping_failed', defaultMessage: 'Cannot connect to the server.', - }; + }); const deviceId = await getDeviceIdForPing(serverUrl, verifyPushProxy); diff --git a/app/client/rest/tracking.ts b/app/client/rest/tracking.ts index 1f250aadc..b0407d0c7 100644 --- a/app/client/rest/tracking.ts +++ b/app/client/rest/tracking.ts @@ -1,11 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {defineMessage} from 'react-intl'; import {DeviceEventEmitter, Platform} from 'react-native'; import {CollectNetworkMetrics} from '@assets/config.json'; import {Events} from '@constants'; -import {t} from '@i18n'; import {setServerCredentials} from '@init/credentials'; import PerformanceMetricsManager from '@managers/performance_metrics_manager'; import {NetworkRequestMetrics} from '@managers/performance_metrics_manager/constant'; @@ -363,10 +363,10 @@ export default class ClientTracking { default: return {error: new ClientError(this.apiClient.baseUrl, { message: 'Invalid request method', - intl: { - id: t('mobile.request.invalid_request_method'), + intl: defineMessage({ + id: 'mobile.request.invalid_request_method', defaultMessage: 'Invalid request method', - }, + }), url, })}; } @@ -382,10 +382,10 @@ export default class ClientTracking { const status_code = isErrorWithStatusCode(error) ? error.status_code : undefined; throw new ClientError(this.apiClient.baseUrl, { message: 'Received invalid response from the server.', - intl: { - id: t('mobile.request.invalid_response'), + intl: defineMessage({ + id: 'mobile.request.invalid_response', defaultMessage: 'Received invalid response from the server.', - }, + }), url, details: error, status_code, diff --git a/app/components/app_version/index.tsx b/app/components/app_version/index.tsx index e84472e18..8536cf64d 100644 --- a/app/components/app_version/index.tsx +++ b/app/components/app_version/index.tsx @@ -3,11 +3,11 @@ import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'; import React, {useEffect} from 'react'; +import {defineMessages} from 'react-intl'; import {Keyboard, StyleSheet, type TextStyle, View} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import FormattedText from '@components/formatted_text'; -import {t} from '@i18n'; const style = StyleSheet.create({ info: { @@ -26,6 +26,13 @@ type AppVersionProps = { textStyle?: TextStyle; } +const messages = defineMessages({ + appVersion: { + id: 'mobile.about.appVersion', + defaultMessage: 'App Version: {version} (Build {number})', + }, +}); + const AppVersion = ({isWrapped = true, textStyle = {}}: AppVersionProps) => { const opacity = useSharedValue(1); const animatedStyle = useAnimatedStyle(() => { @@ -50,8 +57,7 @@ const AppVersion = ({isWrapped = true, textStyle = {}}: AppVersionProps) => { const appVersion = ( { }; })(); +const specialMentionsMessages = defineMessages({ + all: { + id: 'suggestion.mention.all', + defaultMessage: 'Notifies everyone in this channel', + }, + channel: { + id: 'suggestion.mention.channel', + defaultMessage: 'Notifies everyone in this channel', + }, + here: { + id: 'suggestion.mention.here', + defaultMessage: 'Notifies everyone online in this channel', + }, +}); + const getSpecialMentions: () => SpecialMention[] = () => { return [{ completeHandle: 'all', - id: t('suggestion.mention.all'), - defaultMessage: 'Notifies everyone in this channel', + ...specialMentionsMessages.all, }, { completeHandle: 'channel', - id: t('suggestion.mention.channel'), - defaultMessage: 'Notifies everyone in this channel', + ...specialMentionsMessages.channel, }, { completeHandle: 'here', - id: t('suggestion.mention.here'), - defaultMessage: 'Notifies everyone online in this channel', + ...specialMentionsMessages.here, }]; }; @@ -115,14 +128,36 @@ export const filterResults = (users: Array, term: strin }); }; +const sectionMessages = defineMessages({ + members: { + id: 'mobile.suggestion.members', + defaultMessage: 'Members', + }, + groups: { + id: 'suggestion.mention.groups', + defaultMessage: 'Group Mentions', + }, + special: { + id: 'suggestion.mention.special', + defaultMessage: 'Special Mentions', + }, + users: { + id: 'suggestion.mention.users', + defaultMessage: 'Users', + }, + nonmembers: { + id: 'suggestion.mention.nonmembers', + defaultMessage: 'Not in Channel', + }, +}); + export const makeSections = (teamMembers: Array, usersInChannel: Array, usersOutOfChannel: Array, groups: GroupModel[], showSpecialMentions: boolean, isLocal = false, isSearch = false) => { const newSections: UserMentionSections = []; if (isSearch) { if (teamMembers.length) { newSections.push({ - id: t('mobile.suggestion.members'), - defaultMessage: 'Members', + ...sectionMessages.members, data: teamMembers, key: SECTION_KEY_TEAM_MEMBERS, }); @@ -130,8 +165,7 @@ export const makeSections = (teamMembers: Array, usersI } else if (isLocal) { if (teamMembers.length) { newSections.push({ - id: t('mobile.suggestion.members'), - defaultMessage: 'Members', + ...sectionMessages.members, data: teamMembers, key: SECTION_KEY_TEAM_MEMBERS, }); @@ -139,8 +173,7 @@ export const makeSections = (teamMembers: Array, usersI if (groups.length) { newSections.push({ - id: t('suggestion.mention.groups'), - defaultMessage: 'Group Mentions', + ...sectionMessages.groups, data: groups, key: SECTION_KEY_GROUPS, }); @@ -148,8 +181,7 @@ export const makeSections = (teamMembers: Array, usersI if (showSpecialMentions) { newSections.push({ - id: t('suggestion.mention.special'), - defaultMessage: 'Special Mentions', + ...sectionMessages.special, data: getSpecialMentions(), key: SECTION_KEY_SPECIAL, }); @@ -157,8 +189,7 @@ export const makeSections = (teamMembers: Array, usersI } else { if (usersInChannel.length) { newSections.push({ - id: t('suggestion.mention.users'), - defaultMessage: 'Users', + ...sectionMessages.users, data: usersInChannel, key: SECTION_KEY_IN_CHANNEL, }); @@ -166,8 +197,7 @@ export const makeSections = (teamMembers: Array, usersI if (groups.length) { newSections.push({ - id: t('suggestion.mention.groups'), - defaultMessage: 'Group Mentions', + ...sectionMessages.groups, data: groups, key: SECTION_KEY_GROUPS, }); @@ -175,8 +205,7 @@ export const makeSections = (teamMembers: Array, usersI if (showSpecialMentions) { newSections.push({ - id: t('suggestion.mention.special'), - defaultMessage: 'Special Mentions', + ...sectionMessages.special, data: getSpecialMentions(), key: SECTION_KEY_SPECIAL, }); @@ -184,8 +213,7 @@ export const makeSections = (teamMembers: Array, usersI if (usersOutOfChannel.length) { newSections.push({ - id: t('suggestion.mention.nonmembers'), - defaultMessage: 'Not in Channel', + ...sectionMessages.nonmembers, data: usersOutOfChannel, key: SECTION_KEY_OUT_OF_CHANNEL, }); diff --git a/app/components/autocomplete/channel_mention/channel_mention.tsx b/app/components/autocomplete/channel_mention/channel_mention.tsx index 04e6d988f..f4315ff4e 100644 --- a/app/components/autocomplete/channel_mention/channel_mention.tsx +++ b/app/components/autocomplete/channel_mention/channel_mention.tsx @@ -3,6 +3,7 @@ import {debounce} from 'lodash'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {defineMessages} from 'react-intl'; import {Platform, SectionList, type SectionListData, type SectionListRenderItemInfo, type StyleProp, type ViewStyle} from 'react-native'; import {searchChannels} from '@actions/remote/channel'; @@ -13,7 +14,6 @@ import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from '@constants/au import {useServerUrl} from '@context/server'; import DatabaseManager from '@database/manager'; import useDidUpdate from '@hooks/did_update'; -import {t} from '@i18n'; import {getUserById} from '@queries/servers/user'; import {hasTrailingSpaces} from '@utils/helpers'; import {getUserIdFromChannelName} from '@utils/user'; @@ -57,14 +57,36 @@ const reduceChannelsForAutocomplete = (channels: Array, }, [[], []]); }; +const channelMentionMessages = defineMessages({ + public: { + id: 'suggestion.search.public', + defaultMessage: 'Public Channels', + }, + private: { + id: 'suggestion.search.private', + defaultMessage: 'Private Channels', + }, + direct: { + id: 'suggestion.search.direct', + defaultMessage: 'Direct Messages', + }, + channels: { + id: 'suggestion.mention.channels', + defaultMessage: 'My Channels', + }, + morechannels: { + id: 'suggestion.mention.morechannels', + defaultMessage: 'Other Channels', + }, +}); + const makeSections = (channels: Array, myMembers: MyChannelModel[], loading: boolean, isSearch = false) => { const newSections = []; if (isSearch) { const [publicChannels, privateChannels, directAndGroupMessages] = reduceChannelsForSearch(channels, myMembers); if (publicChannels.length) { newSections.push({ - id: t('suggestion.search.public'), - defaultMessage: 'Public Channels', + ...channelMentionMessages.public, data: publicChannels, key: 'publicChannels', hideLoadingIndicator: true, @@ -73,8 +95,7 @@ const makeSections = (channels: Array, myMembers: MyChan if (privateChannels.length) { newSections.push({ - id: t('suggestion.search.private'), - defaultMessage: 'Private Channels', + ...channelMentionMessages.private, data: privateChannels, key: 'privateChannels', hideLoadingIndicator: true, @@ -83,8 +104,7 @@ const makeSections = (channels: Array, myMembers: MyChan if (directAndGroupMessages.length) { newSections.push({ - id: t('suggestion.search.direct'), - defaultMessage: 'Direct Messages', + ...channelMentionMessages.direct, data: directAndGroupMessages, key: 'directAndGroupMessages', hideLoadingIndicator: true, @@ -94,8 +114,7 @@ const makeSections = (channels: Array, myMembers: MyChan const [myChannels, otherChannels] = reduceChannelsForAutocomplete(channels, myMembers); if (myChannels.length) { newSections.push({ - id: t('suggestion.mention.channels'), - defaultMessage: 'My Channels', + ...channelMentionMessages.channels, data: myChannels, key: 'myChannels', hideLoadingIndicator: true, @@ -104,8 +123,7 @@ const makeSections = (channels: Array, myMembers: MyChan if (otherChannels.length || (!myChannels.length && loading)) { newSections.push({ - id: t('suggestion.mention.morechannels'), - defaultMessage: 'Other Channels', + ...channelMentionMessages.morechannels, data: otherChannels, key: 'otherChannels', hideLoadingIndicator: true, @@ -251,7 +269,7 @@ const ChannelMention = ({ setSections(emptySections); setRemoteChannels(emptyChannels); latestSearchAt.current = Date.now(); - }, [value, localCursorPosition, isSearch, currentUserId]); + }, [currentUserId, value, localCursorPosition, isSearch, updateValue, onShowingChange, serverUrl]); const renderItem = useCallback(({item}: SectionListRenderItemInfo) => { return ( diff --git a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts index 2c1406992..fc9610ca2 100644 --- a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts +++ b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts @@ -739,7 +739,7 @@ export class ParsedCommand { if (this.incompleteStart === this.i - 1) { return this.asError(this.intl.formatMessage({ id: 'apps.error.parser.empty_value', - defaultMessage: 'empty values are not allowed', + defaultMessage: 'Empty values are not allowed.', })); } this.i++; @@ -779,7 +779,7 @@ export class ParsedCommand { if (this.incompleteStart === this.i - 1) { return this.asError(this.intl.formatMessage({ id: 'apps.error.parser.empty_value', - defaultMessage: 'empty values are not allowed', + defaultMessage: 'Empty values are not allowed.', })); } this.i++; @@ -1569,7 +1569,7 @@ export class AppCommandParser { const errorResponse = res.error; return {error: errorResponse.text || this.intl.formatMessage({ id: 'apps.error.unknown', - defaultMessage: 'Unknown error.', + defaultMessage: 'Unknown error occurred.', })}; } @@ -1884,7 +1884,7 @@ export class AppCommandParser { const errorResponse = res.error; return this.makeDynamicSelectSuggestionError(errorResponse.text || this.intl.formatMessage({ id: 'apps.error.unknown', - defaultMessage: 'Unknown error.', + defaultMessage: 'Unknown error occurred.', })); } diff --git a/app/components/autocomplete_selector/index.tsx b/app/components/autocomplete_selector/index.tsx index c90c5057b..3c514218a 100644 --- a/app/components/autocomplete_selector/index.tsx +++ b/app/components/autocomplete_selector/index.tsx @@ -14,10 +14,10 @@ import {Screens, View as ViewConstants} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import DatabaseManager from '@database/manager'; +import {usePreventDoubleTap} from '@hooks/utils'; import {getChannelById} from '@queries/servers/channel'; import {getUserById, observeTeammateNameDisplay} from '@queries/servers/user'; import {goToScreen} from '@screens/navigation'; -import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {secureGetFromRecord} from '@utils/types'; import {displayUsername} from '@utils/user'; @@ -157,11 +157,6 @@ function AutoCompleteSelector({ const title = placeholder || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}); const serverUrl = useServerUrl(); - const goToSelectorScreen = useCallback(preventDoubleTap(() => { - const screen = Screens.INTEGRATION_SELECTOR; - goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect}); - }), [dataSource, options, getDynamicOptions]); - const handleSelect = useCallback((newSelection?: Selection) => { if (!newSelection) { return; @@ -184,6 +179,11 @@ function AutoCompleteSelector({ } }, [teammateNameDisplay, intl, dataSource, onSelected]); + const goToSelectorScreen = usePreventDoubleTap(useCallback((() => { + const screen = Screens.INTEGRATION_SELECTOR; + goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect}); + }), [title, dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect])); + // Handle the text for the default value. useEffect(() => { if (!selected) { diff --git a/app/components/channel_actions/leave_channel_label/leave_channel_label.tsx b/app/components/channel_actions/leave_channel_label/leave_channel_label.tsx index 7fd30d20a..d127bd53a 100644 --- a/app/components/channel_actions/leave_channel_label/leave_channel_label.tsx +++ b/app/components/channel_actions/leave_channel_label/leave_channel_label.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; -import {useIntl} from 'react-intl'; +import {defineMessages, useIntl} from 'react-intl'; import {Alert} from 'react-native'; import {leaveChannel} from '@actions/remote/channel'; @@ -23,6 +23,49 @@ type Props = { testID?: string; } +const messages = defineMessages({ + closeDirectMessage: { + id: 'channel_info.close_dm', + defaultMessage: 'Close direct message', + }, + closeGroupMessage: { + id: 'channel_info.close_gm', + defaultMessage: 'Close group message', + }, + closeDirectMessageChannel: { + id: 'channel_info.close_dm_channel', + defaultMessage: 'Are you sure you want to close this direct message? This will remove it from your home screen, but you can always open it again.', + }, + closeGroupMessageChannel: { + id: 'channel_info.close_gm_channel', + defaultMessage: 'Are you sure you want to close this group message? This will remove it from your home screen, but you can always open it again.', + }, + leaveChannel: { + id: 'channel_info.leave_channel', + defaultMessage: 'Leave channel', + }, + leavePrivateChannel: { + id: 'channel_info.leave_private_channel', + defaultMessage: "Are you sure you want to leave the private channel {displayName}? You cannot rejoin the channel unless you're invited again.", + }, + leavePublicChannel: { + id: 'channel_info.leave_public_channel', + defaultMessage: 'Are you sure you want to leave the public channel {displayName}? You can always rejoin.', + }, + cancel: { + id: 'mobile.post.cancel', + defaultMessage: 'Cancel', + }, + close: { + id: 'channel_info.close', + defaultMessage: 'Close', + }, + leave: { + id: 'channel_info.leave', + defaultMessage: 'Leave', + }, +}); + const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => { const intl = useIntl(); const serverUrl = useServerUrl(); @@ -37,16 +80,13 @@ const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type const closeDirectMessage = () => { Alert.alert( - intl.formatMessage({id: 'channel_info.close_dm', defaultMessage: 'Close direct message'}), - intl.formatMessage({ - id: 'channel_info.close_dm_channel', - defaultMessage: 'Are you sure you want to close this direct message? This will remove it from your home screen, but you can always open it again.', - }), + intl.formatMessage(messages.closeDirectMessage), + intl.formatMessage(messages.closeDirectMessageChannel), [{ - text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}), + text: intl.formatMessage(messages.cancel), style: 'cancel', }, { - text: intl.formatMessage({id: 'channel_info.close', defaultMessage: 'Close'}), + text: intl.formatMessage(messages.close), style: 'destructive', onPress: () => { setDirectChannelVisible(serverUrl, channelId, false); @@ -58,16 +98,13 @@ const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type const closeGroupMessage = () => { Alert.alert( - intl.formatMessage({id: 'channel_info.close_gm', defaultMessage: 'Close group message'}), - intl.formatMessage({ - id: 'channel_info.close_gm_channel', - defaultMessage: 'Are you sure you want to close this group message? This will remove it from your home screen, but you can always open it again.', - }), + intl.formatMessage(messages.closeGroupMessage), + intl.formatMessage(messages.closeGroupMessageChannel), [{ - text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}), + text: intl.formatMessage(messages.cancel), style: 'cancel', }, { - text: intl.formatMessage({id: 'channel_info.close', defaultMessage: 'Close'}), + text: intl.formatMessage(messages.close), style: 'destructive', onPress: () => { setDirectChannelVisible(serverUrl, channelId, false); @@ -79,16 +116,13 @@ const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type const leavePublicChannel = () => { Alert.alert( - intl.formatMessage({id: 'channel_info.leave_channel', defaultMessage: 'Leave Channel'}), - intl.formatMessage({ - id: 'channel_info.leave_public_channel', - defaultMessage: 'Are you sure you want to leave the public channel {displayName}? You can always rejoin.', - }, {displayName}), + intl.formatMessage(messages.leaveChannel), + intl.formatMessage(messages.leavePublicChannel, {displayName}), [{ - text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}), + text: intl.formatMessage(messages.cancel), style: 'cancel', }, { - text: intl.formatMessage({id: 'channel_info.leave', defaultMessage: 'Leave'}), + text: intl.formatMessage(messages.leave), style: 'destructive', onPress: () => { leaveChannel(serverUrl, channelId); @@ -100,16 +134,13 @@ const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type const leavePrivateChannel = () => { Alert.alert( - intl.formatMessage({id: 'channel_info.leave_channel', defaultMessage: 'Leave Channel'}), - intl.formatMessage({ - id: 'channel_info.leave_private_channel', - defaultMessage: "Are you sure you want to leave the private channel {displayName}? You cannot rejoin the channel unless you're invited again.", - }, {displayName}), + intl.formatMessage(messages.leaveChannel), + intl.formatMessage(messages.leavePrivateChannel, {displayName}), [{ - text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}), + text: intl.formatMessage(messages.cancel), style: 'cancel', }, { - text: intl.formatMessage({id: 'channel_info.leave', defaultMessage: 'Leave'}), + text: intl.formatMessage(messages.leave), style: 'destructive', onPress: () => { leaveChannel(serverUrl, channelId); @@ -144,15 +175,15 @@ const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type let icon; switch (type) { case General.DM_CHANNEL: - leaveText = intl.formatMessage({id: 'channel_info.close_dm', defaultMessage: 'Close direct message'}); + leaveText = intl.formatMessage(messages.closeDirectMessage); icon = 'close'; break; case General.GM_CHANNEL: - leaveText = intl.formatMessage({id: 'channel_info.close_gm', defaultMessage: 'Close group message'}); + leaveText = intl.formatMessage(messages.closeGroupMessage); icon = 'close'; break; default: - leaveText = intl.formatMessage({id: 'channel_info.leave_channel', defaultMessage: 'Leave channel'}); + leaveText = intl.formatMessage(messages.leaveChannel); icon = 'exit-to-app'; break; } diff --git a/app/components/channel_actions/manage_members_label/manage_members_label.tsx b/app/components/channel_actions/manage_members_label/manage_members_label.tsx index dc3cc559f..1f0a20138 100644 --- a/app/components/channel_actions/manage_members_label/manage_members_label.tsx +++ b/app/components/channel_actions/manage_members_label/manage_members_label.tsx @@ -31,7 +31,7 @@ const messages = defineMessages({ }, remove_title: { id: 'mobile.manage_members.remove_member', - defaultMessage: 'Remove From Channel', + defaultMessage: 'Remove from Channel', }, remove_message: { id: 'mobile.manage_members.message', diff --git a/app/components/channel_bookmarks/add_bookmark.tsx b/app/components/channel_bookmarks/add_bookmark.tsx index 32bc92957..e5c0a29f4 100644 --- a/app/components/channel_bookmarks/add_bookmark.tsx +++ b/app/components/channel_bookmarks/add_bookmark.tsx @@ -88,8 +88,8 @@ const AddBookmark = ({bookmarksCount, channelId, currentUserId, canUploadFiles, formatMessage({id: 'channel_info.add_bookmark', defaultMessage: 'Add a bookmark'}), formatMessage({ id: 'channel_info.add_bookmark.max_reached', - defaultMessage: 'This channel has reached the maximum number of bookmarks ({count}).', - }, {count: MAX_BOOKMARKS_PER_CHANNEL}), + defaultMessage: 'This channel has reached the maximum number of bookmarks.', + }), ); return; } diff --git a/app/components/common_post_options/base_option/index.tsx b/app/components/common_post_options/base_option/index.tsx index df85900db..58d12b124 100644 --- a/app/components/common_post_options/base_option/index.tsx +++ b/app/components/common_post_options/base_option/index.tsx @@ -2,13 +2,12 @@ // See LICENSE.txt for license information. import React from 'react'; -import {useIntl} from 'react-intl'; +import {useIntl, type MessageDescriptor} from 'react-intl'; import OptionItem from '@components/option_item'; type BaseOptionType = { - defaultMessage: string; - i18nId: string; + message: MessageDescriptor; iconName: string; isDestructive?: boolean; onPress: () => void; @@ -16,8 +15,7 @@ type BaseOptionType = { } const BaseOption = ({ - defaultMessage, - i18nId, + message, iconName, isDestructive = false, onPress, @@ -30,7 +28,7 @@ const BaseOption = ({ action={onPress} destructive={isDestructive} icon={iconName} - label={intl.formatMessage({id: i18nId, defaultMessage})} + label={intl.formatMessage(message)} testID={testID} type='default' /> diff --git a/app/components/common_post_options/copy_permalink_option/copy_permalink_option.tsx b/app/components/common_post_options/copy_permalink_option/copy_permalink_option.tsx index 07b9c6a86..43887ee39 100644 --- a/app/components/common_post_options/copy_permalink_option/copy_permalink_option.tsx +++ b/app/components/common_post_options/copy_permalink_option/copy_permalink_option.tsx @@ -3,11 +3,11 @@ import Clipboard from '@react-native-clipboard/clipboard'; import React, {useCallback} from 'react'; +import {defineMessages} from 'react-intl'; import {BaseOption} from '@components/common_post_options'; import {SNACK_BAR_TYPE} from '@constants/snack_bar'; import {useServerUrl} from '@context/server'; -import {t} from '@i18n'; import {dismissBottomSheet} from '@screens/navigation'; import {showSnackBar} from '@utils/snack_bar'; @@ -20,6 +20,14 @@ type Props = { post: PostModel; teamName: string; } + +const messages = defineMessages({ + copyLink: { + id: 'get_post_link_modal.title', + defaultMessage: 'Copy Link', + }, +}); + const CopyPermalinkOption = ({bottomSheetId, teamName, post, sourceScreen}: Props) => { const serverUrl = useServerUrl(); @@ -28,12 +36,11 @@ const CopyPermalinkOption = ({bottomSheetId, teamName, post, sourceScreen}: Prop Clipboard.setString(permalink); await dismissBottomSheet(bottomSheetId); showSnackBar({barType: SNACK_BAR_TYPE.LINK_COPIED, sourceScreen}); - }, [teamName, post.id, bottomSheetId]); + }, [serverUrl, teamName, post.id, bottomSheetId, sourceScreen]); return ( { - let id: string; - let defaultMessage: string; + let message: MessageDescriptor; let icon: string; if (thread.isFollowing) { icon = 'message-minus-outline'; if (thread.replyCount) { - id = t('threads.unfollowThread'); - defaultMessage = 'Unfollow Thread'; + message = messages.unfollowThread; } else { - id = t('threads.unfollowMessage'); - defaultMessage = 'Unfollow Message'; + message = messages.unfollowMessage; } } else { icon = 'message-plus-outline'; if (thread.replyCount) { - id = t('threads.followThread'); - defaultMessage = 'Follow Thread'; + message = messages.followThread; } else { - id = t('threads.followMessage'); - defaultMessage = 'Follow Message'; + message = messages.followMessage; } } @@ -51,14 +64,13 @@ const FollowThreadOption = ({bottomSheetId, thread, teamId}: FollowThreadOptionP } await dismissBottomSheet(bottomSheetId); updateThreadFollowing(serverUrl, teamId, thread.id, !thread.isFollowing, true); - }, [bottomSheetId, teamId, thread]); + }, [bottomSheetId, serverUrl, teamId, thread.id, thread.isFollowing]); const followThreadOptionTestId = thread.isFollowing ? 'post_options.following_thread.option' : 'post_options.follow_thread.option'; return ( { const serverUrl = useServerUrl(); @@ -27,8 +34,7 @@ const ReplyOption = ({post, bottomSheetId}: Props) => { return ( { const serverUrl = useServerUrl(); @@ -24,18 +35,16 @@ const SaveOption = ({bottomSheetId, isSaved, postId}: CopyTextProps) => { const remoteAction = isSaved ? deleteSavedPost : savePostPreference; await dismissBottomSheet(bottomSheetId); remoteAction(serverUrl, postId); - }, [bottomSheetId, postId, serverUrl]); + }, [bottomSheetId, isSaved, postId, serverUrl]); - const id = isSaved ? t('mobile.post_info.unsave') : t('mobile.post_info.save'); - const defaultMessage = isSaved ? 'Unsave' : 'Save'; + const message = isSaved ? messages.unsave : messages.save; return ( ); }; diff --git a/app/components/copy_text_option/index.tsx b/app/components/copy_text_option/index.tsx index 5f4440464..2d3080857 100644 --- a/app/components/copy_text_option/index.tsx +++ b/app/components/copy_text_option/index.tsx @@ -3,11 +3,11 @@ import Clipboard from '@react-native-clipboard/clipboard'; import React, {useCallback} from 'react'; +import {defineMessages} from 'react-intl'; import {Platform} from 'react-native'; import {BaseOption} from '@components/common_post_options'; import {SNACK_BAR_TYPE} from '@constants/snack_bar'; -import {t} from '@i18n'; import {dismissBottomSheet} from '@screens/navigation'; import {showSnackBar} from '@utils/snack_bar'; @@ -18,6 +18,14 @@ type Props = { sourceScreen: AvailableScreens; postMessage: string; } + +const messages = defineMessages({ + copyText: { + id: 'mobile.post_info.copy_text', + defaultMessage: 'Copy Text', + }, +}); + const CopyTextOption = ({bottomSheetId, postMessage, sourceScreen}: Props) => { const handleCopyText = useCallback(async () => { Clipboard.setString(postMessage); @@ -25,12 +33,11 @@ const CopyTextOption = ({bottomSheetId, postMessage, sourceScreen}: Props) => { if ((Platform.OS === 'android' && Platform.Version < 33) || Platform.OS === 'ios') { showSnackBar({barType: SNACK_BAR_TYPE.MESSAGE_COPIED, sourceScreen}); } - }, [postMessage]); + }, [bottomSheetId, postMessage, sourceScreen]); return ( { const ClearButton = ({handlePress, iconName = 'close-circle', size = 20, containerSize = 40, theme, testID}: Props) => { const style = getStyleSheet(theme); + const onPress = usePreventDoubleTap(handlePress); + return ( diff --git a/app/components/files/file_filter.tsx b/app/components/files/file_filter.tsx index 55ba51fa7..615ae46d8 100644 --- a/app/components/files/file_filter.tsx +++ b/app/components/files/file_filter.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useCallback} from 'react'; -import {useIntl} from 'react-intl'; +import {defineMessages, useIntl} from 'react-intl'; import {type ListRenderItemInfo, View} from 'react-native'; import {FlatList} from 'react-native-gesture-handler'; @@ -10,7 +10,6 @@ import OptionItem, {ITEM_HEIGHT} from '@components/option_item'; import {useTheme} from '@context/theme'; import {useBottomSheetListsFix} from '@hooks/bottom_sheet_lists_fix'; import {useIsTablet} from '@hooks/device'; -import {t} from '@i18n'; import BottomSheetContent from '@screens/bottom_sheet/content'; import {dismissBottomSheet} from '@screens/navigation'; import {type FileFilter, FileFilters} from '@utils/file'; @@ -25,6 +24,41 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); +const filterMessages = defineMessages({ + allFileTypes: { + id: 'screen.search.results.filter.all_file_types', + defaultMessage: 'All file types', + }, + documents: { + id: 'screen.search.results.filter.documents', + defaultMessage: 'Documents', + }, + spreadsheets: { + id: 'screen.search.results.filter.spreadsheets', + defaultMessage: 'Spreadsheets', + }, + presentations: { + id: 'screen.search.results.filter.presentations', + defaultMessage: 'Presentations', + }, + code: { + id: 'screen.search.results.filter.code', + defaultMessage: 'Code', + }, + images: { + id: 'screen.search.results.filter.images', + defaultMessage: 'Images', + }, + audio: { + id: 'screen.search.results.filter.audio', + defaultMessage: 'Audio', + }, + videos: { + id: 'screen.search.results.filter.videos', + defaultMessage: 'Videos', + }, +}); + type FilterItem = { id: string; defaultMessage: string; @@ -33,43 +67,35 @@ type FilterItem = { } export const FilterData = { [FileFilters.ALL]: { - id: t('screen.search.results.filter.all_file_types'), - defaultMessage: 'All file types', + ...filterMessages.allFileTypes, filterType: FileFilters.ALL, }, [FileFilters.DOCUMENTS]: { - id: t('screen.search.results.filter.documents'), - defaultMessage: 'Documents', + ...filterMessages.documents, filterType: FileFilters.DOCUMENTS, }, [FileFilters.SPREADSHEETS]: { - id: t('screen.search.results.filter.spreadsheets'), - defaultMessage: 'Spreadsheets', + ...filterMessages.spreadsheets, filterType: FileFilters.SPREADSHEETS, }, [FileFilters.PRESENTATIONS]: { - id: t('screen.search.results.filter.presentations'), - defaultMessage: 'Presentations', + ...filterMessages.presentations, filterType: FileFilters.PRESENTATIONS, }, [FileFilters.CODE]: { - id: t('screen.search.results.filter.code'), - defaultMessage: 'Code', + ...filterMessages.code, filterType: FileFilters.CODE, }, [FileFilters.IMAGES]: { - id: t('screen.search.results.filter.images'), - defaultMessage: 'Images', + ...filterMessages.images, filterType: FileFilters.IMAGES, }, [FileFilters.AUDIO]: { - id: t('screen.search.results.filter.audio'), - defaultMessage: 'Audio', + ...filterMessages.audio, filterType: FileFilters.AUDIO, }, [FileFilters.VIDEOS]: { - id: t('screen.search.results.filter.videos'), - defaultMessage: 'Videos', + ...filterMessages.videos, filterType: FileFilters.VIDEOS, separator: false, }, diff --git a/app/components/markdown/channel_mention/channel_mention.tsx b/app/components/markdown/channel_mention/channel_mention.tsx index 554dfbf1b..18e59306b 100644 --- a/app/components/markdown/channel_mention/channel_mention.tsx +++ b/app/components/markdown/channel_mention/channel_mention.tsx @@ -1,15 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; -import {useIntl} from 'react-intl'; +import React, {useCallback} from 'react'; +import {defineMessage, useIntl} from 'react-intl'; import {type StyleProp, Text, type TextStyle} from 'react-native'; import {joinChannel, switchToChannelById} from '@actions/remote/channel'; import {useServerUrl} from '@context/server'; -import {t} from '@i18n'; +import {usePreventDoubleTap} from '@hooks/utils'; import {alertErrorWithFallback} from '@utils/draft'; -import {preventDoubleTap} from '@utils/tap'; import {secureGetFromRecord, isRecordOf} from '@utils/types'; import type ChannelModel from '@typings/database/models/servers/channel'; @@ -90,16 +89,16 @@ const ChannelMention = ({ const serverUrl = useServerUrl(); const channel = getChannelFromChannelName(channelName, channels, channelMentions, team.name); - const handlePress = preventDoubleTap(async () => { + const handlePress = usePreventDoubleTap(useCallback((async () => { let c = channel; if (!c?.id && c?.display_name) { const result = await joinChannel(serverUrl, currentTeamId, undefined, channelName); if (result.error || !result.channel) { - const joinFailedMessage = { - id: t('mobile.join_channel.error'), + const joinFailedMessage = defineMessage({ + id: 'mobile.join_channel.error', defaultMessage: "We couldn't join the channel {displayName}.", - }; + }); alertErrorWithFallback(intl, result.error || {}, joinFailedMessage, {displayName: c.display_name}); } else if (result.channel) { c = { @@ -113,7 +112,7 @@ const ChannelMention = ({ if (c?.id) { switchToChannelById(serverUrl, c.id); } - }); + }), [channel, channelName, currentTeamId, intl, serverUrl])); if (!channel) { return {`~${channelName}`}; diff --git a/app/components/markdown/markdown_code_block/index.tsx b/app/components/markdown/markdown_code_block/index.tsx index d6ace5432..fbc749f78 100644 --- a/app/components/markdown/markdown_code_block/index.tsx +++ b/app/components/markdown/markdown_code_block/index.tsx @@ -11,10 +11,10 @@ import FormattedText from '@components/formatted_text'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import {Screens} from '@constants'; import {useTheme} from '@context/theme'; +import {usePreventDoubleTap} from '@hooks/utils'; import {bottomSheet, dismissBottomSheet, goToScreen} from '@screens/navigation'; import {bottomSheetSnapPoint} from '@utils/helpers'; import {getHighlightLanguageFromNameOrAlias, getHighlightLanguageName} from '@utils/markdown'; -import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import type {SyntaxHiglightProps} from '@typings/components/syntax_highlight'; @@ -79,7 +79,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc return syntaxHighlighter; }, []); - const handlePress = useCallback(preventDoubleTap(() => { + const handlePress = usePreventDoubleTap(useCallback(() => { const screen = Screens.CODE; const passProps = { code: content, @@ -110,7 +110,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc requestAnimationFrame(() => { goToScreen(screen, title, passProps); }); - }), [content, intl.locale, language]); + }, [content, intl, language, textStyle])); const handleLongPress = useCallback(() => { if (managedConfig?.copyAndPasteProtection !== 'true') { diff --git a/app/components/markdown/markdown_latex_block/index.tsx b/app/components/markdown/markdown_latex_block/index.tsx index 772506a5a..4defb266f 100644 --- a/app/components/markdown/markdown_latex_block/index.tsx +++ b/app/components/markdown/markdown_latex_block/index.tsx @@ -13,11 +13,11 @@ import MathView from '@components/math_view'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {Screens} from '@constants'; +import {usePreventDoubleTap} from '@hooks/utils'; import {bottomSheet, dismissBottomSheet, goToScreen} from '@screens/navigation'; import {bottomSheetSnapPoint} from '@utils/helpers'; import {getHighlightLanguageName} from '@utils/markdown'; import {splitLatexCodeInLines} from '@utils/markdown/latex'; -import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -110,7 +110,7 @@ const LatexCodeBlock = ({content, theme}: Props) => { }; }, [content]); - const handlePress = useCallback(preventDoubleTap(() => { + const handlePress = usePreventDoubleTap(useCallback(() => { const screen = Screens.LATEX; const passProps = { content, @@ -126,7 +126,7 @@ const LatexCodeBlock = ({content, theme}: Props) => { requestAnimationFrame(() => { goToScreen(screen, title, passProps); }); - }), [content, languageDisplayName, intl.locale]); + }, [content, intl, languageDisplayName])); const handleLongPress = useCallback(() => { if (managedConfig?.copyAndPasteProtection !== 'true') { @@ -168,7 +168,7 @@ const LatexCodeBlock = ({content, theme}: Props) => { const onRenderErrorMessage = useCallback(({error}: {error: Error}) => { return {'Render error: ' + error.message}; - }, []); + }, [styles.errorText]); let plusMoreLines = null; if (split.numberOfLines > MAX_LINES) { diff --git a/app/components/no_results_with_term/index.tsx b/app/components/no_results_with_term/index.tsx index 3320fe73f..8e3801cd0 100644 --- a/app/components/no_results_with_term/index.tsx +++ b/app/components/no_results_with_term/index.tsx @@ -1,12 +1,12 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useState, useEffect} from 'react'; +import React from 'react'; +import {defineMessages} from 'react-intl'; import {View} from 'react-native'; import FormattedText from '@components/formatted_text'; import {useTheme} from '@context/theme'; -import {t} from '@i18n'; import {TabTypes, type TabType} from '@utils/search'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -42,24 +42,28 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { }; }); +const messages = defineMessages({ + files: { + id: 'mobile.no_results_with_term.files', + defaultMessage: 'No files matching “{term}”', + }, + messages: { + id: 'mobile.no_results_with_term.messages', + defaultMessage: 'No matches found for “{term}”', + }, +}); + const NoResultsWithTerm = ({term, type}: Props) => { const theme = useTheme(); const style = getStyleFromTheme(theme); - const [titleId, setTitleId] = useState(t('mobile.no_results_with_term')); - const [defaultMessage, setDefaultMessage] = useState('No results for “{term}”'); - - useEffect(() => { - setTitleId(type === TabTypes.FILES ? t('mobile.no_results_with_term.files') : t('mobile.no_results_with_term.messages')); - setDefaultMessage(type === TabTypes.FILES ? 'No files matching “{term}”' : 'No matches found for “{term}”'); - }, [type]); + const titleMessage = type === TabTypes.FILES ? messages.files : messages.messages; return ( {type === TabTypes.FILES ? : } diff --git a/app/components/post_draft/archived/index.tsx b/app/components/post_draft/archived/index.tsx index d11b9ceaf..b631980fa 100644 --- a/app/components/post_draft/archived/index.tsx +++ b/app/components/post_draft/archived/index.tsx @@ -3,6 +3,7 @@ import {Button} from '@rneui/base'; import React, {useCallback} from 'react'; +import {defineMessages} from 'react-intl'; import {type Edge, SafeAreaView} from 'react-native-safe-area-context'; import {switchToPenultimateChannel} from '@actions/remote/channel'; @@ -11,7 +12,6 @@ import FormattedText from '@components/formatted_text'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; -import {t} from '@i18n'; import {popToRoot} from '@screens/navigation'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -57,6 +57,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ const edges: Edge[] = ['bottom']; +const messages = defineMessages({ + archivedChannelMessage: { + id: 'archivedChannelMessage', + defaultMessage: 'You are viewing an **archived channel**. New messages cannot be posted.', + }, + deactivatedChannelMessage: { + id: 'create_post.deactivated', + defaultMessage: 'You are viewing an archived channel with a deactivated user.', + }, +}); + export default function Archived({ testID, deactivated, @@ -75,17 +86,11 @@ export default function Archived({ } }, [serverUrl, isTablet]); - let message = { - id: t('archivedChannelMessage'), - defaultMessage: 'You are viewing an **archived channel**. New messages cannot be posted.', - }; + let message = messages.archivedChannelMessage; if (deactivated) { // only applies to DM's when the user was deactivated - message = { - id: t('create_post.deactivated'), - defaultMessage: 'You are viewing an archived channel with a deactivated user.', - }; + message = messages.deactivatedChannelMessage; } return ( diff --git a/app/components/post_draft/post_input/post_input.tsx b/app/components/post_draft/post_input/post_input.tsx index 35ae48f9e..115578be7 100644 --- a/app/components/post_draft/post_input/post_input.tsx +++ b/app/components/post_draft/post_input/post_input.tsx @@ -5,7 +5,7 @@ import {useHardwareKeyboardEvents} from '@mattermost/hardware-keyboard'; import {useManagedConfig} from '@mattermost/react-native-emm'; import PasteableTextInput, {type PastedFile, type PasteInputRef} from '@mattermost/react-native-paste-input'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {type IntlShape, useIntl} from 'react-intl'; +import {defineMessage, type IntlShape, useIntl} from 'react-intl'; import { Alert, AppState, type AppStateStatus, DeviceEventEmitter, type EmitterSubscription, Keyboard, type NativeSyntheticEvent, Platform, type TextInputSelectionChangeEventData, @@ -19,7 +19,6 @@ import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {useInputPropagation} from '@hooks/input'; -import {t} from '@i18n'; import NavigationStore from '@store/navigation_store'; import {handleDraftUpdate} from '@utils/draft'; import {extractFileInfo} from '@utils/file'; @@ -72,9 +71,9 @@ const getPlaceHolder = (rootId?: string) => { let placeholder; if (rootId) { - placeholder = {id: t('create_post.thread_reply'), defaultMessage: 'Reply to this thread...'}; + placeholder = defineMessage({id: 'create_post.thread_reply', defaultMessage: 'Reply to this thread...'}); } else { - placeholder = {id: t('create_post.write'), defaultMessage: 'Write to {channelDisplayName}'}; + placeholder = defineMessage({id: 'create_post.write', defaultMessage: 'Write to {channelDisplayName}'}); } return placeholder; @@ -214,12 +213,16 @@ export default function PostInput({ lastTypingEventSent.current = Date.now(); } }, [ + shouldProcessEvent, updateValue, checkMessageLength, timeBetweenUserTypingUpdatesMilliseconds, + membersInChannel, + maxNotificationsPerChannel, + enableUserTypingMessage, + serverUrl, channelId, rootId, - (membersInChannel < maxNotificationsPerChannel) && enableUserTypingMessage, ]); const onPaste = useCallback(async (error: string | null | undefined, files: PastedFile[]) => { diff --git a/app/components/post_list/combined_user_activity/messages.ts b/app/components/post_list/combined_user_activity/messages.ts index 8c0ea0b9e..75388b1e5 100644 --- a/app/components/post_list/combined_user_activity/messages.ts +++ b/app/components/post_list/combined_user_activity/messages.ts @@ -1,8 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {defineMessages} from 'react-intl'; + import {Post} from '@constants'; -import {t} from '@i18n'; const { JOIN_CHANNEL, ADD_TO_CHANNEL, REMOVE_FROM_CHANNEL, LEAVE_CHANNEL, @@ -10,183 +11,183 @@ const { } = Post.POST_TYPES; export const postTypeMessages = { - [JOIN_CHANNEL]: { + [JOIN_CHANNEL]: defineMessages({ one: { - id: t('combined_system_message.joined_channel.one'), + id: 'combined_system_message.joined_channel.one', defaultMessage: '{firstUser} **joined the channel**.', }, one_you: { - id: t('combined_system_message.joined_channel.one_you'), + id: 'combined_system_message.joined_channel.one_you', defaultMessage: 'You **joined the channel**.', }, two: { - id: t('combined_system_message.joined_channel.two'), + id: 'combined_system_message.joined_channel.two', defaultMessage: '{firstUser} and {secondUser} **joined the channel**.', }, many_expanded: { - id: t('combined_system_message.joined_channel.many_expanded'), + id: 'combined_system_message.joined_channel.many_expanded', defaultMessage: '{users} and {lastUser} **joined the channel**.', }, - }, - [ADD_TO_CHANNEL]: { + }), + [ADD_TO_CHANNEL]: defineMessages({ one: { - id: t('combined_system_message.added_to_channel.one'), + id: 'combined_system_message.added_to_channel.one', defaultMessage: '{firstUser} **added to the channel** by {actor}.', }, one_you: { - id: t('combined_system_message.added_to_channel.one_you'), + id: 'combined_system_message.added_to_channel.one_you', defaultMessage: 'You were **added to the channel** by {actor}.', }, two: { - id: t('combined_system_message.added_to_channel.two'), + id: 'combined_system_message.added_to_channel.two', defaultMessage: '{firstUser} and {secondUser} **added to the channel** by {actor}.', }, many_expanded: { - id: t('combined_system_message.added_to_channel.many_expanded'), + id: 'combined_system_message.added_to_channel.many_expanded', defaultMessage: '{users} and {lastUser} were **added to the channel** by {actor}.', }, - }, - [REMOVE_FROM_CHANNEL]: { + }), + [REMOVE_FROM_CHANNEL]: defineMessages({ one: { - id: t('combined_system_message.removed_from_channel.one'), + id: 'combined_system_message.removed_from_channel.one', defaultMessage: '{firstUser} was **removed from the channel**.', }, one_you: { - id: t('combined_system_message.removed_from_channel.one_you'), + id: 'combined_system_message.removed_from_channel.one_you', defaultMessage: 'You were **removed from the channel**.', }, two: { - id: t('combined_system_message.removed_from_channel.two'), + id: 'combined_system_message.removed_from_channel.two', defaultMessage: '{firstUser} and {secondUser} were **removed from the channel**.', }, many_expanded: { - id: t('combined_system_message.removed_from_channel.many_expanded'), + id: 'combined_system_message.removed_from_channel.many_expanded', defaultMessage: '{users} and {lastUser} were **removed from the channel**.', }, - }, - [LEAVE_CHANNEL]: { + }), + [LEAVE_CHANNEL]: defineMessages({ one: { - id: t('combined_system_message.left_channel.one'), + id: 'combined_system_message.left_channel.one', defaultMessage: '{firstUser} **left the channel**.', }, one_you: { - id: t('combined_system_message.left_channel.one_you'), + id: 'combined_system_message.left_channel.one_you', defaultMessage: 'You **left the channel**.', }, two: { - id: t('combined_system_message.left_channel.two'), + id: 'combined_system_message.left_channel.two', defaultMessage: '{firstUser} and {secondUser} **left the channel**.', }, many_expanded: { - id: t('combined_system_message.left_channel.many_expanded'), + id: 'combined_system_message.left_channel.many_expanded', defaultMessage: '{users} and {lastUser} **left the channel**.', }, - }, - [JOIN_TEAM]: { + }), + [JOIN_TEAM]: defineMessages({ one: { - id: t('combined_system_message.joined_team.one'), + id: 'combined_system_message.joined_team.one', defaultMessage: '{firstUser} **joined the team**.', }, one_you: { - id: t('combined_system_message.joined_team.one_you'), + id: 'combined_system_message.joined_team.one_you', defaultMessage: 'You **joined the team**.', }, two: { - id: t('combined_system_message.joined_team.two'), + id: 'combined_system_message.joined_team.two', defaultMessage: '{firstUser} and {secondUser} **joined the team**.', }, many_expanded: { - id: t('combined_system_message.joined_team.many_expanded'), + id: 'combined_system_message.joined_team.many_expanded', defaultMessage: '{users} and {lastUser} **joined the team**.', }, - }, - [ADD_TO_TEAM]: { + }), + [ADD_TO_TEAM]: defineMessages({ one: { - id: t('combined_system_message.added_to_team.one'), + id: 'combined_system_message.added_to_team.one', defaultMessage: '{firstUser} **added to the team** by {actor}.', }, one_you: { - id: t('combined_system_message.added_to_team.one_you'), + id: 'combined_system_message.added_to_team.one_you', defaultMessage: 'You were **added to the team** by {actor}.', }, two: { - id: t('combined_system_message.added_to_team.two'), + id: 'combined_system_message.added_to_team.two', defaultMessage: '{firstUser} and {secondUser} **added to the team** by {actor}.', }, many_expanded: { - id: t('combined_system_message.added_to_team.many_expanded'), + id: 'combined_system_message.added_to_team.many_expanded', defaultMessage: '{users} and {lastUser} were **added to the team** by {actor}.', }, - }, - [REMOVE_FROM_TEAM]: { + }), + [REMOVE_FROM_TEAM]: defineMessages({ one: { - id: t('combined_system_message.removed_from_team.one'), + id: 'combined_system_message.removed_from_team.one', defaultMessage: '{firstUser} was **removed from the team**.', }, one_you: { - id: t('combined_system_message.removed_from_team.one_you'), + id: 'combined_system_message.removed_from_team.one_you', defaultMessage: 'You were **removed from the team**.', }, two: { - id: t('combined_system_message.removed_from_team.two'), + id: 'combined_system_message.removed_from_team.two', defaultMessage: '{firstUser} and {secondUser} were **removed from the team**.', }, many_expanded: { - id: t('combined_system_message.removed_from_team.many_expanded'), + id: 'combined_system_message.removed_from_team.many_expanded', defaultMessage: '{users} and {lastUser} were **removed from the team**.', }, - }, - [LEAVE_TEAM]: { + }), + [LEAVE_TEAM]: defineMessages({ one: { - id: t('combined_system_message.left_team.one'), + id: 'combined_system_message.left_team.one', defaultMessage: '{firstUser} **left the team**.', }, one_you: { - id: t('combined_system_message.left_team.one_you'), + id: 'combined_system_message.left_team.one_you', defaultMessage: 'You **left the team**.', }, two: { - id: t('combined_system_message.left_team.two'), + id: 'combined_system_message.left_team.two', defaultMessage: '{firstUser} and {secondUser} **left the team**.', }, many_expanded: { - id: t('combined_system_message.left_team.many_expanded'), + id: 'combined_system_message.left_team.many_expanded', defaultMessage: '{users} and {lastUser} **left the team**.', }, - }, + }), }; -export const systemMessages = { +export const systemMessages = defineMessages({ [ADD_TO_CHANNEL]: { - id: t('last_users_message.added_to_channel.type'), + id: 'last_users_message.added_to_channel.type', defaultMessage: 'were **added to the channel** by {actor}.', }, [JOIN_CHANNEL]: { - id: t('last_users_message.joined_channel.type'), + id: 'last_users_message.joined_channel.type', defaultMessage: '**joined the channel**.', }, [LEAVE_CHANNEL]: { - id: t('last_users_message.left_channel.type'), + id: 'last_users_message.left_channel.type', defaultMessage: '**left the channel**.', }, [REMOVE_FROM_CHANNEL]: { - id: t('last_users_message.removed_from_channel.type'), + id: 'last_users_message.removed_from_channel.type', defaultMessage: 'were **removed from the channel**.', }, [ADD_TO_TEAM]: { - id: t('last_users_message.added_to_team.type'), + id: 'last_users_message.added_to_team.type', defaultMessage: 'were **added to the team** by {actor}.', }, [JOIN_TEAM]: { - id: t('last_users_message.joined_team.type'), + id: 'last_users_message.joined_team.type', defaultMessage: '**joined the team**.', }, [LEAVE_TEAM]: { - id: t('last_users_message.left_team.type'), + id: 'last_users_message.left_team.type', defaultMessage: '**left the team**.', }, [REMOVE_FROM_TEAM]: { - id: t('last_users_message.removed_from_team.type'), + id: 'last_users_message.removed_from_team.type', defaultMessage: 'were **removed from the team**.', }, -}; +}); diff --git a/app/components/post_list/post/avatar/avatar.tsx b/app/components/post_list/post/avatar/avatar.tsx index d08873d58..2d9e2bb55 100644 --- a/app/components/post_list/post/avatar/avatar.tsx +++ b/app/components/post_list/post/avatar/avatar.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {Image} from 'expo-image'; -import React, {type ReactNode} from 'react'; +import React, {useCallback, type ReactNode} from 'react'; import {useIntl} from 'react-intl'; import {Platform, StyleSheet, TouchableOpacity, View} from 'react-native'; @@ -12,8 +12,8 @@ import ProfilePicture from '@components/profile_picture'; import {View as ViewConstant} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; +import {usePreventDoubleTap} from '@hooks/utils'; import {openUserProfileModal} from '@screens/navigation'; -import {preventDoubleTap} from '@utils/tap'; import {ensureString} from '@utils/types'; import type PostModel from '@typings/database/models/servers/post'; @@ -88,7 +88,7 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}: ); } - const openUserProfile = preventDoubleTap(() => { + const openUserProfile = usePreventDoubleTap(useCallback(() => { if (!author) { return; } @@ -99,7 +99,7 @@ const Avatar = ({author, enablePostIconOverride, isAutoReponse, location, post}: userIconOverride: propsIconUrl, usernameOverride: propsUsername, }); - }); + }, [author, intl, location, post.channelId, propsIconUrl, propsUsername, theme])); let component = ( const serverUrl = useServerUrl(); const style = getStyleSheet(theme); - const onPress = useCallback(preventDoubleTap(async () => { + const onPress = usePreventDoubleTap(useCallback(async () => { if (pressed.current) { return; } @@ -118,7 +118,7 @@ const ButtonBinding = ({currentTeamId, binding, post, teamID, theme}: Props) => postEphemeralCallResponseForPost(serverUrl, callResp, errorMessage, post); } } - }), []); + }, [binding, currentTeamId, intl, post, serverUrl, teamID])); return (