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
This commit is contained in:
Daniel Espino García 2025-08-25 12:03:01 +02:00 committed by GitHub
parent 1965b4777d
commit 0c4a42a06a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
142 changed files with 1716 additions and 1106 deletions

View file

@ -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);
}

View file

@ -81,7 +81,7 @@ export async function doAppSubmit<Res=unknown>(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<Res>(errMsg)};
}
@ -133,7 +133,7 @@ export async function doAppFetchForm<Res=unknown>(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<Res>(errMsg)};
}

View file

@ -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 () => {

View file

@ -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;

View file

@ -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);

View file

@ -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,

View file

@ -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 = (
<FormattedText
id={t('mobile.about.appVersion')}
defaultMessage='App Version: {version} (Build {number})'
{...messages.appVersion}
style={StyleSheet.flatten([style.version, textStyle])}
values={{
version: nativeApplicationVersion,

View file

@ -1,10 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {defineMessages} from 'react-intl';
import {searchGroupsByName, searchGroupsByNameInChannel, searchGroupsByNameInTeam} from '@actions/local/group';
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
import DatabaseManager from '@database/manager';
import {t} from '@i18n';
import {getUsersFromDMSorted, queryAllUsers} from '@queries/servers/user';
import {hasTrailingSpaces} from '@utils/helpers';
@ -39,19 +40,31 @@ export const getMatchTermForAtMention = (() => {
};
})();
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<UserModel | UserProfile>, 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<UserProfile | UserModel>, usersInChannel: Array<UserProfile | UserModel>, usersOutOfChannel: Array<UserProfile | UserModel>, 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<UserProfile | UserModel>, 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<UserProfile | UserModel>, 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<UserProfile | UserModel>, 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<UserProfile | UserModel>, 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<UserProfile | UserModel>, 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<UserProfile | UserModel>, 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<UserProfile | UserModel>, 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,
});

View file

@ -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<Channel | ChannelModel>,
}, [[], []]);
};
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<Channel | ChannelModel>, 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<Channel | ChannelModel>, 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<Channel | ChannelModel>, 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<Channel | ChannelModel>, 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<Channel | ChannelModel>, 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<Channel | ChannelModel>) => {
return (

View file

@ -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.',
}));
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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',

View file

@ -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;
}

View file

@ -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'
/>

View file

@ -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 (
<BaseOption
i18nId={t('get_post_link_modal.title')}
defaultMessage='Copy Link'
message={messages.copyLink}
onPress={handleCopyLink}
iconName='link-variant'
testID='post_options.copy_permalink.option'

View file

@ -2,11 +2,11 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {defineMessages, type MessageDescriptor} from 'react-intl';
import {updateThreadFollowing} from '@actions/remote/thread';
import {BaseOption} from '@components/common_post_options';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import type ThreadModel from '@typings/database/models/servers/thread';
@ -18,28 +18,41 @@ type FollowThreadOptionProps = {
teamId?: string;
};
const messages = defineMessages({
unfollowThread: {
id: 'threads.unfollowThread',
defaultMessage: 'Unfollow Thread',
},
followThread: {
id: 'threads.followThread',
defaultMessage: 'Follow Thread',
},
followMessage: {
id: 'threads.followMessage',
defaultMessage: 'Follow Message',
},
unfollowMessage: {
id: 'threads.unfollowMessage',
defaultMessage: 'Unfollow Message',
},
});
const FollowThreadOption = ({bottomSheetId, thread, teamId}: FollowThreadOptionProps) => {
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 (
<BaseOption
i18nId={id}
defaultMessage={defaultMessage}
message={message}
testID={followThreadOptionTestId}
iconName={icon}
onPress={handleToggleFollow}

View file

@ -2,11 +2,11 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {defineMessages} from 'react-intl';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import {BaseOption} from '@components/common_post_options';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import type PostModel from '@typings/database/models/servers/post';
@ -16,6 +16,13 @@ type Props = {
post: PostModel;
bottomSheetId: AvailableScreens;
}
const messages = defineMessages({
reply: {
id: 'mobile.post_info.reply',
defaultMessage: 'Reply',
},
});
const ReplyOption = ({post, bottomSheetId}: Props) => {
const serverUrl = useServerUrl();
@ -27,8 +34,7 @@ const ReplyOption = ({post, bottomSheetId}: Props) => {
return (
<BaseOption
i18nId={t('mobile.post_info.reply')}
defaultMessage='Reply'
message={messages.reply}
iconName='reply-outline'
onPress={handleReply}
testID='post_options.reply_post.option'

View file

@ -2,11 +2,11 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {defineMessages} from 'react-intl';
import {deleteSavedPost, savePostPreference} from '@actions/remote/preference';
import {BaseOption} from '@components/common_post_options';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import type {AvailableScreens} from '@typings/screens/navigation';
@ -17,6 +17,17 @@ type CopyTextProps = {
postId: string;
}
const messages = defineMessages({
save: {
id: 'mobile.post_info.save',
defaultMessage: 'Save',
},
unsave: {
id: 'mobile.post_info.unsave',
defaultMessage: 'Unsave',
},
});
const SaveOption = ({bottomSheetId, isSaved, postId}: CopyTextProps) => {
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 (
<BaseOption
i18nId={id}
defaultMessage={defaultMessage}
message={message}
iconName='bookmark-outline'
onPress={onHandlePress}
testID={`post_options.${defaultMessage.toLocaleLowerCase()}_post.option`}
testID={`post_options.${message.defaultMessage.toLocaleLowerCase()}_post.option`}
/>
);
};

View file

@ -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 (
<BaseOption
i18nId={t('mobile.post_info.copy_text')}
defaultMessage='Copy Text'
message={messages.copyText}
iconName='content-copy'
onPress={handleCopyText}
testID='post_options.copy_text.option'

View file

@ -5,7 +5,7 @@ import React from 'react';
import {TouchableOpacity} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
interface Props {
@ -34,9 +34,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const ClearButton = ({handlePress, iconName = 'close-circle', size = 20, containerSize = 40, theme, testID}: Props) => {
const style = getStyleSheet(theme);
const onPress = usePreventDoubleTap(handlePress);
return (
<TouchableOpacity
onPress={preventDoubleTap(handlePress)}
onPress={onPress}
style={[style.container, {height: containerSize, width: containerSize}]}
testID={testID}
>

View file

@ -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,
},

View file

@ -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 <Text style={textStyle}>{`~${channelName}`}</Text>;

View file

@ -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') {

View file

@ -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 <Text style={styles.errorText}>{'Render error: ' + error.message}</Text>;
}, []);
}, [styles.errorText]);
let plusMoreLines = null;
if (split.numberOfLines > MAX_LINES) {

View file

@ -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 (
<View style={style.container}>
{type === TabTypes.FILES ? <SearchFilesIllustration/> : <SearchIllustration/>}
<FormattedText
id={titleId}
defaultMessage={defaultMessage}
{...titleMessage}
style={style.result}
values={{term}}
/>

View file

@ -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 (

View file

@ -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[]) => {

View file

@ -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**.',
},
};
});

View file

@ -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 = (
<ProfilePicture

View file

@ -81,11 +81,11 @@ const definedMessages = defineMessages({
},
messageOne: {
id: 'post_body.check_for_out_of_channel_mentions.message.one',
defaultMessage: '{username} was mentioned but is not in the channel. Would you like to ',
defaultMessage: 'was mentioned but is not in the channel. Would you like to ',
},
messageMultiple: {
id: 'post_body.check_for_out_of_channel_mentions.message.multiple',
defaultMessage: '{usernames} were mentioned but they are not in the channel. Would you like to ',
defaultMessage: 'were mentioned but they are not in the channel. Would you like to ',
},
outOfGroupsMessage: {
id: 'post_body.check_for_out_of_channel_groups_mentions.message',

View file

@ -11,12 +11,12 @@ import {handleBindingClick, postEphemeralCallResponseForPost} from '@actions/rem
import {handleGotoLocation} from '@actions/remote/command';
import {AppBindingLocations, AppCallResponseTypes} from '@constants/apps';
import {useServerUrl} from '@context/server';
import {usePreventDoubleTap} from '@hooks/utils';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {showAppForm} from '@screens/navigation';
import {createCallContext} from '@utils/apps';
import {getStatusColors} from '@utils/message_attachment';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import ButtonBindingText from './button_binding_text';
@ -62,7 +62,7 @@ const ButtonBinding = ({currentTeamId, binding, post, teamID, theme}: Props) =>
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 (
<Button

View file

@ -6,8 +6,8 @@ import React, {useCallback, useRef} from 'react';
import {postActionWithCookie} from '@actions/remote/integrations';
import {useServerUrl} from '@context/server';
import {usePreventDoubleTap} from '@hooks/utils';
import {getStatusColors} from '@utils/message_attachment';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
@ -56,13 +56,13 @@ const ActionButton = ({buttonColor, cookie, disabled, id, name, postId, theme}:
let customButtonStyle;
let customButtonTextStyle;
const onPress = useCallback(preventDoubleTap(async () => {
const onPress = usePreventDoubleTap(useCallback(async () => {
if (!presssed.current) {
presssed.current = true;
await postActionWithCookie(serverUrl, postId, id, cookie || '');
presssed.current = false;
}
}), [id, postId, cookie]);
}, [serverUrl, postId, id, cookie]));
if (buttonColor) {
const STATUS_COLORS = getStatusColors(theme);

View file

@ -12,9 +12,9 @@ import {MAX_ALLOWED_REACTIONS} from '@constants/emoji';
import {useServerUrl} from '@context/server';
import {useIsTablet} from '@hooks/device';
import useDidUpdate from '@hooks/did_update';
import {usePreventDoubleTap} from '@hooks/utils';
import {bottomSheetModalOptions, openAsBottomSheet, showModal, showModalOverCurrentContext} from '@screens/navigation';
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import Reaction from './reaction';
@ -100,11 +100,11 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
return {reactionsByName, highlightedReactions};
}, [reactions, currentUserId]);
const handleToggleReactionToPost = (emoji: string) => {
const handleToggleReactionToPost = useCallback((emoji: string) => {
toggleReaction(serverUrl, postId, emoji);
};
}, [postId, serverUrl]);
const handleAddReaction = useCallback(preventDoubleTap(() => {
const handleAddReaction = usePreventDoubleTap(useCallback(() => {
openAsBottomSheet({
closeButtonId: 'close-add-reaction',
screen: Screens.EMOJI_PICKER,
@ -112,7 +112,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
props: {onEmojiPress: handleToggleReactionToPost},
});
}), [formatMessage, theme]);
}, [formatMessage, handleToggleReactionToPost, theme]));
const handleReactionPress = useCallback(async (emoji: string, remove: boolean) => {
pressed.current = true;
@ -123,7 +123,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
}
pressed.current = false;
}, [canRemoveReaction, canAddReaction, disabled]);
}, [canRemoveReaction, disabled, canAddReaction, serverUrl, postId]);
const showReactionList = useCallback((initialEmoji: string) => {
const screen = Screens.REACTIONS;

View file

@ -2,12 +2,12 @@
// See LICENSE.txt for license information.
import React from 'react';
import {defineMessages} from 'react-intl';
import {View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {makeStyleSheetFromTheme} from '@utils/theme';
type PreHeaderProps = {
@ -56,6 +56,21 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
const messages = defineMessages({
pinnedSaved: {
id: 'mobile.post_pre_header.pinned_saved',
defaultMessage: 'Pinned and Saved',
},
pinned: {
id: 'mobile.post_pre_header.pinned',
defaultMessage: 'Pinned',
},
saved: {
id: 'mobile.post_pre_header.saved',
defaultMessage: 'Saved',
},
});
const PreHeader = ({isConsecutivePost, isSaved, isPinned, skipSavedHeader, skipPinnedHeader}: PreHeaderProps) => {
const theme = useTheme();
const style = getStyleSheet(theme);
@ -63,20 +78,11 @@ const PreHeader = ({isConsecutivePost, isSaved, isPinned, skipSavedHeader, skipP
let text;
if (isPinnedAndSaved) {
text = {
id: t('mobile.post_pre_header.pinned_saved'),
defaultMessage: 'Pinned and Saved',
};
text = messages.pinnedSaved;
} else if (isPinned && !skipPinnedHeader) {
text = {
id: t('mobile.post_pre_header.pinned'),
defaultMessage: 'Pinned',
};
text = messages.pinned;
} else if (isSaved && !skipSavedHeader) {
text = {
id: t('mobile.post_pre_header.saved'),
defaultMessage: 'Saved',
};
text = messages.saved;
}
if (!text) {

View file

@ -2,14 +2,13 @@
// See LICENSE.txt for license information.
import React from 'react';
import {type IntlShape, useIntl} from 'react-intl';
import {defineMessages, type IntlShape, useIntl} from 'react-intl';
import {type StyleProp, Text, type TextStyle, View, type ViewStyle} from 'react-native';
import Markdown from '@components/markdown';
import {postTypeMessages} from '@components/post_list/combined_user_activity/messages';
import {Post} from '@constants';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord, ensureString} from '@utils/types';
@ -94,6 +93,21 @@ const renderMessage = ({location, post, styles, intl, localeHolder, theme, value
);
};
const headerMessages = defineMessages({
updatedFrom: {
id: 'mobile.system_message.update_channel_header_message_and_forget.updated_from',
defaultMessage: '{username} updated the channel header from: {oldHeader} to: {newHeader}',
},
updatedTo: {
id: 'mobile.system_message.update_channel_header_message_and_forget.updated_to',
defaultMessage: '{username} updated the channel header to: {newHeader}',
},
removed: {
id: 'mobile.system_message.update_channel_header_message_and_forget.removed',
defaultMessage: '{username} removed the channel header (was: {oldHeader})',
},
});
const renderHeaderChangeMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
let values;
@ -108,27 +122,18 @@ const renderHeaderChangeMessage = ({post, author, location, styles, intl, theme}
if (post.props?.new_header) {
if (post.props?.old_header) {
localeHolder = {
id: t('mobile.system_message.update_channel_header_message_and_forget.updated_from'),
defaultMessage: '{username} updated the channel header from: {oldHeader} to: {newHeader}',
};
localeHolder = headerMessages.updatedFrom;
values = {username, oldHeader, newHeader};
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
}
localeHolder = {
id: t('mobile.system_message.update_channel_header_message_and_forget.updated_to'),
defaultMessage: '{username} updated the channel header to: {newHeader}',
};
localeHolder = headerMessages.updatedTo;
values = {username, oldHeader, newHeader};
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
} else if (post.props?.old_header) {
localeHolder = {
id: t('mobile.system_message.update_channel_header_message_and_forget.removed'),
defaultMessage: '{username} removed the channel header (was: {oldHeader})',
};
localeHolder = headerMessages.removed;
values = {username, oldHeader, newHeader};
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
@ -137,6 +142,21 @@ const renderHeaderChangeMessage = ({post, author, location, styles, intl, theme}
return null;
};
const purposeMessages = defineMessages({
updatedFrom: {
id: 'mobile.system_message.update_channel_purpose_message.updated_from',
defaultMessage: '{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}',
},
updatedTo: {
id: 'mobile.system_message.update_channel_purpose_message.updated_to',
defaultMessage: '{username} updated the channel purpose to: {newPurpose}',
},
removed: {
id: 'mobile.system_message.update_channel_purpose_message.removed',
defaultMessage: '{username} removed the channel purpose (was: {oldPurpose})',
},
});
const renderPurposeChangeMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
let values;
@ -151,27 +171,18 @@ const renderPurposeChangeMessage = ({post, author, location, styles, intl, theme
if (newPurpose) {
if (oldPurpose) {
localeHolder = {
id: t('mobile.system_message.update_channel_purpose_message.updated_from'),
defaultMessage: '{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}',
};
localeHolder = purposeMessages.updatedFrom;
values = {username, oldPurpose, newPurpose};
return renderMessage({post, styles, intl, location, localeHolder, values, skipMarkdown: true, theme});
}
localeHolder = {
id: t('mobile.system_message.update_channel_purpose_message.updated_to'),
defaultMessage: '{username} updated the channel purpose to: {newPurpose}',
};
localeHolder = purposeMessages.updatedTo;
values = {username, oldPurpose, newPurpose};
return renderMessage({post, styles, intl, location, localeHolder, values, skipMarkdown: true, theme});
} else if (oldPurpose) {
localeHolder = {
id: t('mobile.system_message.update_channel_purpose_message.removed'),
defaultMessage: '{username} removed the channel purpose (was: {oldPurpose})',
};
localeHolder = purposeMessages.removed;
values = {username, oldPurpose, newPurpose};
return renderMessage({post, styles, intl, location, localeHolder, values, skipMarkdown: true, theme});
@ -180,6 +191,13 @@ const renderPurposeChangeMessage = ({post, author, location, styles, intl, theme
return null;
};
const displaynameMessages = defineMessages({
updatedFrom: {
id: 'mobile.system_message.update_channel_displayname_message_and_forget.updated_from',
defaultMessage: '{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}',
},
});
const renderDisplayNameChangeMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
const oldDisplayName = ensureString(post.props?.old_displayname);
const newDisplayName = ensureString(post.props?.new_displayname);
@ -189,41 +207,53 @@ const renderDisplayNameChangeMessage = ({post, author, location, styles, intl, t
}
const username = renderUsername(author.username);
const localeHolder = {
id: t('mobile.system_message.update_channel_displayname_message_and_forget.updated_from'),
defaultMessage: '{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}',
};
const localeHolder = displaynameMessages.updatedFrom;
const values = {username, oldDisplayName, newDisplayName};
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
};
const archivedMessages = defineMessages({
archived: {
id: 'mobile.system_message.channel_archived_message',
defaultMessage: '{username} archived the channel',
},
});
const renderArchivedMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
const username = renderUsername(author?.username);
const localeHolder = {
id: t('mobile.system_message.channel_archived_message'),
defaultMessage: '{username} archived the channel',
};
const localeHolder = archivedMessages.archived;
const values = {username};
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
};
const unarchivedMessages = defineMessages({
unarchived: {
id: 'mobile.system_message.channel_unarchived_message',
defaultMessage: '{username} unarchived the channel',
},
});
const renderUnarchivedMessage = ({post, author, location, styles, intl, theme}: RenderersProps) => {
if (!author?.username) {
return null;
}
const username = renderUsername(author.username);
const localeHolder = {
id: t('mobile.system_message.channel_unarchived_message'),
defaultMessage: '{username} unarchived the channel',
};
const localeHolder = unarchivedMessages.unarchived;
const values = {username};
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
};
const addGuestToChannelMessages = defineMessages({
added: {
id: 'api.channel.add_guest.added',
defaultMessage: '{addedUsername} added to the channel as a guest by {username}.',
},
});
const renderAddGuestToChannelMessage = ({post, location, styles, intl, theme}: RenderersProps, hideGuestTags: boolean) => {
const username = renderUsername(ensureString(post.props?.username));
const addedUsername = renderUsername(ensureString(post.props?.addedUsername));
@ -232,25 +262,26 @@ const renderAddGuestToChannelMessage = ({post, location, styles, intl, theme}: R
return null;
}
const localeHolder = hideGuestTags ? postTypeMessages[Post.POST_TYPES.ADD_TO_CHANNEL].one : {
id: t('api.channel.add_guest.added'),
defaultMessage: '{addedUsername} added to the channel as a guest by {username}.',
};
const localeHolder = hideGuestTags ? postTypeMessages[Post.POST_TYPES.ADD_TO_CHANNEL].one : addGuestToChannelMessages.added;
const values = hideGuestTags ? {firstUser: addedUsername, actor: username} : {username, addedUsername};
return renderMessage({post, styles, intl, location, localeHolder, values, theme});
};
const guestJoinChannelMessages = defineMessages({
joined: {
id: 'api.channel.guest_join_channel.post_and_forget',
defaultMessage: '{username} joined the channel as a guest.',
},
});
const renderGuestJoinChannelMessage = ({post, styles, location, intl, theme}: RenderersProps, hideGuestTags: boolean) => {
const username = renderUsername(ensureString(post.props?.username));
if (!username) {
return null;
}
const localeHolder = hideGuestTags ? postTypeMessages[Post.POST_TYPES.JOIN_CHANNEL].one : {
id: t('api.channel.guest_join_channel.post_and_forget'),
defaultMessage: '{username} joined the channel as a guest.',
};
const localeHolder = hideGuestTags ? postTypeMessages[Post.POST_TYPES.JOIN_CHANNEL].one : guestJoinChannelMessages.joined;
const values = hideGuestTags ? {firstUser: username} : {username};
return renderMessage({post, styles, intl, location, localeHolder, values, theme});

View file

@ -13,8 +13,8 @@ import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {usePreventDoubleTap} from '@hooks/utils';
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -65,14 +65,14 @@ const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props)
const isTablet = useIsTablet();
const serverUrl = useServerUrl();
const onHandleSavePress = useCallback(preventDoubleTap(() => {
const onHandleSavePress = usePreventDoubleTap(useCallback(() => {
if (rootPost?.id) {
const remoteAction = isSaved ? deleteSavedPost : savePostPreference;
remoteAction(serverUrl, rootPost.id);
}
}), [isSaved, rootPost, serverUrl]);
}, [isSaved, rootPost, serverUrl]));
const showPostOptions = useCallback(preventDoubleTap(() => {
const showPostOptions = usePreventDoubleTap(useCallback(() => {
Keyboard.dismiss();
if (rootPost?.id) {
const passProps = {sourceScreen: Screens.THREAD, post: rootPost, showAddReaction: true};
@ -84,7 +84,7 @@ const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props)
showModalOverCurrentContext(Screens.POST_OPTIONS, passProps, bottomSheetModalOptions(theme));
}
}
}), [rootPost]);
}, [intl, isTablet, rootPost, theme]));
const containerStyle = useMemo(() => {
const container: StyleProp<ViewStyle> = [styles.container];
@ -95,7 +95,7 @@ const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props)
}
container.push(style);
return container;
}, [repliesCount, style]);
}, [repliesCount, style, styles.container]);
const saveButtonTestId = isSaved ? `${testID}.unsave.button` : `${testID}.save.button`;

View file

@ -2,12 +2,12 @@
// See LICENSE.txt for license information.
import {Image, type ImageSource, type ImageStyle} from 'expo-image';
import React, {useCallback} from 'react';
import React from 'react';
import {type StyleProp, Text, type TextStyle, TouchableHighlight, View, type ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {useTheme} from '@context/theme';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {isValidUrl} from '@utils/url';
@ -86,7 +86,7 @@ const SlideUpPanelItem = ({
const {image: leftImage, iconStyle: leftIconStyle} = useImageAndStyle(leftIcon, leftImageStyles, leftIconStyles, destructive);
const {image: rightImage, iconStyle: rightIconStyle} = useImageAndStyle(rightIcon, rightImageStyles, rightIconStyles, destructive);
const handleOnPress = useCallback(preventDoubleTap(onPress, 500), []);
const handleOnPress = usePreventDoubleTap(onPress);
return (
<TouchableHighlight

View file

@ -2,11 +2,11 @@
// See LICENSE.txt for license information.
import React from 'react';
import {defineMessages} from 'react-intl';
import FormattedText from '@components/formatted_text';
import {General} from '@constants';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -28,37 +28,53 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
};
});
const messages = defineMessages({
setAway: {
id: 'status_dropdown.set_away',
defaultMessage: 'Away',
},
setDnd: {
id: 'status_dropdown.set_dnd',
defaultMessage: 'Do Not Disturb',
},
setOnline: {
id: 'status_dropdown.set_online',
defaultMessage: 'Online',
},
setOffline: {
id: 'status_dropdown.set_offline',
defaultMessage: 'Offline',
},
setOoo: {
id: 'status_dropdown.set_ooo',
defaultMessage: 'Out Of Office',
},
});
const StatusLabel = ({status = General.OFFLINE, labelStyle}: StatusLabelProps) => {
const theme = useTheme();
const style = getStyleSheet(theme);
let i18nId = t('status_dropdown.set_offline');
let defaultMessage = 'Offline';
let message = messages.setOffline;
switch (status) {
case General.AWAY:
i18nId = t('status_dropdown.set_away');
defaultMessage = 'Away';
message = messages.setAway;
break;
case General.DND:
i18nId = t('status_dropdown.set_dnd');
defaultMessage = 'Do Not Disturb';
message = messages.setDnd;
break;
case General.ONLINE:
i18nId = t('status_dropdown.set_online');
defaultMessage = 'Online';
message = messages.setOnline;
break;
}
if (status === General.OUT_OF_OFFICE) {
i18nId = t('status_dropdown.set_ooo');
defaultMessage = 'Out Of Office';
message = messages.setOoo;
}
return (
<FormattedText
id={i18nId}
defaultMessage={defaultMessage}
{...message}
style={[style.label, labelStyle]}
testID={`user_status.label.${status}`}
/>

View file

@ -9,8 +9,8 @@ import CompassIcon from '@components/compass_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {usePreventDoubleTap} from '@hooks/utils';
import {showModal} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -40,7 +40,7 @@ export default function AddTeam() {
const styles = getStyleSheet(theme);
const intl = useIntl();
const onPress = useCallback(preventDoubleTap(() => {
const onPress = usePreventDoubleTap(useCallback(() => {
const title = intl.formatMessage({id: 'mobile.add_team.join_team', defaultMessage: 'Join Another Team'});
const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor);
const closeButtonId = 'close-join-team';
@ -54,7 +54,7 @@ export default function AddTeam() {
},
};
showModal(Screens.JOIN_TEAM, title, {closeButtonId}, options);
}), [intl]);
}, [intl, theme.sidebarHeaderTextColor]));
return (
<View style={styles.container}>

View file

@ -6,7 +6,7 @@ import {type StyleProp, View, type ViewStyle} from 'react-native';
import Emoji from '@components/emoji';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import SkinnedEmoji from './skinned_emoji';
@ -23,7 +23,7 @@ const CATEGORIES_WITH_SKINS = ['people-body'];
const hitSlop = {top: 10, bottom: 10, left: 10, right: 10};
const TouchableEmoji = ({category, name, onEmojiPress, size = 30, style}: Props) => {
const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
const onPress = usePreventDoubleTap(useCallback(() => onEmojiPress(name), [name, onEmojiPress]));
if (category && CATEGORIES_WITH_SKINS.includes(category)) {
return (

View file

@ -7,9 +7,9 @@ import {type StyleProp, View, type ViewStyle} from 'react-native';
import Emoji from '@components/emoji';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useEmojiSkinTone} from '@hooks/emoji_category_bar';
import {usePreventDoubleTap} from '@hooks/utils';
import {skinCodes} from '@utils/emoji';
import {isValidNamedEmoji} from '@utils/emoji/helpers';
import {preventDoubleTap} from '@utils/tap';
type Props = {
name: string;
@ -30,9 +30,9 @@ const SkinnedEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
return skinnedEmoji;
}, [name, skinTone]);
const onPress = useCallback(preventDoubleTap(() => {
const onPress = usePreventDoubleTap(useCallback(() => {
onEmojiPress(emojiName);
}), [emojiName]);
}, [emojiName, onEmojiPress]));
return (
<View

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {
InteractionManager,
Platform,
@ -17,7 +17,6 @@ import TutorialLongPress from '@components/tutorial_highlight/long_press';
import UserItem from '@components/user_item';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {t} from '@i18n';
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import {typography} from '@utils/typography';
@ -69,6 +68,17 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
const DEFAULT_ICON_OPACITY = 0.32;
const messages = defineMessages({
admin: {
id: 'mobile.manage_members.admin',
defaultMessage: 'Admin',
},
member: {
id: 'mobile.manage_members.member',
defaultMessage: 'Member',
},
});
function UserListRow({
id,
includeMargin,
@ -124,15 +134,13 @@ function UserListRow({
}
const color = changeOpacity(theme.centerChannelColor, 0.64);
const i18nId = isChannelAdmin ? t('mobile.manage_members.admin') : t('mobile.manage_members.member');
const defaultMessage = isChannelAdmin ? 'Admin' : 'Member';
const message = isChannelAdmin ? messages.admin : messages.member;
return (
<View style={style.selectorManage}>
<FormattedText
id={i18nId}
{...message}
style={style.manageText}
defaultMessage={defaultMessage}
/>
<CompassIcon
name={'chevron-down'}
@ -141,7 +149,7 @@ function UserListRow({
/>
</View>
);
}, [isChannelAdmin, showManageMode, theme]);
}, [isChannelAdmin, isMyUser, showManageMode, style.manageText, style.selectorManage, theme.centerChannelColor]);
const onLayout = useCallback(() => {
if (highlight && !tutorialWatched) {
@ -153,7 +161,7 @@ function UserListRow({
setShowTutorial(true);
});
}
}, [showTutorial]);
}, [highlight, isTablet, tutorialWatched]);
useLayoutEffect(() => {
if (showTutorial && !tutorialShown.current) {
@ -177,7 +185,7 @@ function UserListRow({
/>
</View>
);
}, [selectable, disabled, selected, theme]);
}, [selectable, selected, theme.buttonBg, theme.centerChannelColor, style.selector]);
const userItemTestID = `${testID}.${id}`;

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {t} from '@i18n';
import {defineMessages} from 'react-intl';
export enum CustomStatusDurationEnum {
DONT_CLEAR = '',
@ -23,36 +23,36 @@ const {
DATE_AND_TIME,
} = CustomStatusDurationEnum;
export const CST: {[key in CustomStatusDuration]: {id: string; defaultMessage: string}} = {
export const CST: {[key in CustomStatusDuration]: {id: string; defaultMessage: string}} = defineMessages({
[DONT_CLEAR]: {
id: t('custom_status.expiry_dropdown.dont_clear'),
id: 'custom_status.expiry_dropdown.dont_clear',
defaultMessage: "Don't clear",
},
[THIRTY_MINUTES]: {
id: t('custom_status.expiry_dropdown.thirty_minutes'),
id: 'custom_status.expiry_dropdown.thirty_minutes',
defaultMessage: '30 minutes',
},
[ONE_HOUR]: {
id: t('custom_status.expiry_dropdown.one_hour'),
id: 'custom_status.expiry_dropdown.one_hour',
defaultMessage: '1 hour',
},
[FOUR_HOURS]: {
id: t('custom_status.expiry_dropdown.four_hours'),
id: 'custom_status.expiry_dropdown.four_hours',
defaultMessage: '4 hours',
},
[TODAY]: {
id: t('custom_status.expiry_dropdown.today'),
id: 'custom_status.expiry_dropdown.today',
defaultMessage: 'Today',
},
[THIS_WEEK]: {
id: t('custom_status.expiry_dropdown.this_week'),
id: 'custom_status.expiry_dropdown.this_week',
defaultMessage: 'This week',
},
[DATE_AND_TIME]: {
id: t('custom_status.expiry_dropdown.date_and_time'),
id: 'custom_status.expiry_dropdown.date_and_time',
defaultMessage: 'Date and Time',
},
};
});
export const CUSTOM_STATUS_TEXT_CHARACTER_LIMIT = 100;

View file

@ -8,8 +8,8 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Screens} from '@constants';
import {useIsTablet} from '@hooks/device';
import {usePreventDoubleTap} from '@hooks/utils';
import NavigationStore from '@store/navigation_store';
import {preventDoubleTap} from '@utils/tap';
import type {AvailableScreens} from '@typings/screens/navigation';
@ -144,7 +144,7 @@ export const useExtraKeyboardContext = (): ExtraKeyboardContextProps|undefined =
export const useHideExtraKeyboardIfNeeded = (callback: (...args: any) => void, dependencies: React.DependencyList = []) => {
const keyboardContext = useExtraKeyboardContext();
return useCallback(preventDoubleTap((...args: any) => {
return usePreventDoubleTap(useCallback((...args: any) => {
if (keyboardContext?.isExtraKeyboardVisible) {
keyboardContext.hideExtraKeyboard();
@ -162,7 +162,7 @@ export const useHideExtraKeyboardIfNeeded = (callback: (...args: any) => void, d
}
callback(...args);
}), [keyboardContext, ...dependencies]);
}, [keyboardContext, ...dependencies]));
};
const ExtraKeyboardComponent = () => {

View file

@ -14,7 +14,7 @@ export function privateChannelJoinPrompt(displayName: string, intl: IntlShape):
}),
intl.formatMessage({
id: 'permalink.show_dialog_warn.description',
defaultMessage: 'You are about to join {channel} without explicitly being added by the Channel Admin. Are you sure you wish to join this private channel?',
defaultMessage: 'You are about to join {channel} without explicitly being added by the channel admin. Are you sure you wish to join this private channel?',
}, {
channel: displayName,
}),

View file

@ -245,7 +245,3 @@ export function getLocalizedMessage(lang: string, id: string, defaultMessage?: s
return translations[id] || defaultMessage || '';
}
export function t(v: string): string {
return v;
}

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import RNUtils from '@mattermost/rnutils/src';
import {defineMessages} from 'react-intl';
import {AppState, DeviceEventEmitter, Platform, type EmitterSubscription} from 'react-native';
import {
Notification,
@ -23,7 +24,7 @@ import {backgroundNotification, openNotification} from '@actions/remote/notifica
import {isCallsStartedMessage} from '@calls/utils';
import {Device, Events, Navigation, PushNotification, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
import {DEFAULT_LOCALE, getLocalizedMessage} from '@i18n';
import {getServerDisplayName} from '@queries/app/servers';
import {getCurrentChannelId} from '@queries/servers/system';
import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread';
@ -35,6 +36,21 @@ import {isMainActivity, isTablet} from '@utils/helpers';
import {logDebug, logInfo} from '@utils/log';
import {convertToNotificationData} from '@utils/notification';
const messages = defineMessages({
replyTitle: {
id: 'mobile.push_notification_reply.title',
defaultMessage: 'Reply',
},
replyButton: {
id: 'mobile.push_notification_reply.button',
defaultMessage: 'Send',
},
replyPlaceholder: {
id: 'mobile.push_notification_reply.placeholder',
defaultMessage: 'Write a reply...',
},
});
class PushNotificationsSingleton {
configured = false;
subscriptions?: EmitterSubscription[];
@ -64,9 +80,9 @@ class PushNotificationsSingleton {
}
createReplyCategory = () => {
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.title'));
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.button'));
const replyPlaceholder = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.placeholder'));
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, messages.replyTitle.id);
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, messages.replyButton.id);
const replyPlaceholder = getLocalizedMessage(DEFAULT_LOCALE, messages.replyPlaceholder.id);
const replyTextInput: NotificationTextInput = {buttonTitle: replyButton, placeholder: replyPlaceholder};
const replyAction = new NotificationAction(PushNotification.REPLY_ACTION, 'background', replyTitle, true, replyTextInput);
return new NotificationCategory(PushNotification.CATEGORY, [replyAction]);

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import RNUtils, {type SplitViewResult} from '@mattermost/rnutils';
import {defineMessages} from 'react-intl';
import {Alert, DeviceEventEmitter, Linking, NativeEventEmitter} from 'react-native';
import semver from 'semver';
@ -9,7 +10,7 @@ import {switchToChannelById} from '@actions/remote/channel';
import {Device, Events, Sso} from '@constants';
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
import {getServerCredentials} from '@init/credentials';
import {getActiveServerUrl} from '@queries/app/servers';
import {queryTeamDefaultChannel} from '@queries/servers/channel';
@ -23,6 +24,21 @@ type LinkingCallbackArg = {url: string};
const splitViewEmitter = new NativeEventEmitter(RNUtils);
const messages = defineMessages({
serverUpgradeTitle: {
id: 'mobile.server_upgrade.title',
defaultMessage: 'Server upgrade required',
},
serverUpgradeDescription: {
id: 'mobile.server_upgrade.description',
defaultMessage: '\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n',
},
serverUpgradeButton: {
id: 'mobile.server_upgrade.button',
defaultMessage: 'OK',
},
});
class GlobalEventHandlerSingleton {
JavascriptAndNativeErrorHandler: jsAndNativeErrorHandler | undefined;
@ -59,10 +75,10 @@ class GlobalEventHandlerSingleton {
if (version) {
if (semver.valid(version) && semver.lt(version, MIN_REQUIRED_VERSION)) {
Alert.alert(
translations[t('mobile.server_upgrade.title')],
translations[t('mobile.server_upgrade.description')],
translations[messages.serverUpgradeTitle.id],
translations[messages.serverUpgradeDescription.id],
[{
text: translations[t('mobile.server_upgrade.button')],
text: translations[messages.serverUpgradeButton.id],
onPress: () => this.serverUpgradeNeeded(serverUrl),
}],
{cancelable: false},

View file

@ -8,13 +8,15 @@ import {Alert, type AppStateStatus} from 'react-native';
import {switchToServer} from '@actions/app/server';
import {logout} from '@actions/remote/session';
import {Screens} from '@constants';
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
import {getServerCredentials} from '@init/credentials';
import TestHelper from '@test/test_helper';
import {toMilliseconds} from '@utils/datetime';
import {logError} from '@utils/log';
import SecurityManager from '.';
import SecurityManager, {exportsForTesting} from '.';
const messages = exportsForTesting.messages;
jest.mock('@mattermost/react-native-emm', () => ({
isDeviceSecured: jest.fn(),
@ -442,7 +444,7 @@ describe('SecurityManager', () => {
const translations = getTranslations(DEFAULT_LOCALE);
const buttons = await SecurityManager.buildAlertOptions('server-12', translations);
expect(buttons.length).toBeGreaterThan(0);
const logoutButton = buttons.find((button) => button.text === translations[t('mobile.managed.logout')]);
const logoutButton = buttons.find((button) => button.text === translations[messages.logout.id]);
expect(logoutButton).toBeDefined();
logoutButton?.onPress?.();
expect(logout).toHaveBeenCalledWith('server-12', undefined);
@ -458,8 +460,8 @@ describe('SecurityManager', () => {
await SecurityManager.showDeviceNotTrustedAlert(server, siteName, translations);
expect(Alert.alert).toHaveBeenCalledWith(
translations[t('mobile.managed.blocked_by')].replace('{vendor}', siteName),
translations[t('mobile.managed.jailbreak')].replace('{vendor}', siteName),
translations[messages.blocked_by.id].replace('{vendor}', siteName),
translations[messages.jailbreak.id].replace('{vendor}', siteName),
expect.any(Array),
{cancelable: false},
);
@ -475,8 +477,8 @@ describe('SecurityManager', () => {
await SecurityManager.showBiometricFailureAlert(server, false, siteName, translations);
expect(Alert.alert).toHaveBeenCalledWith(
translations[t('mobile.managed.blocked_by')].replace('{vendor}', siteName),
translations[t('mobile.managed.biometric_failed')],
translations[messages.blocked_by.id].replace('{vendor}', siteName),
translations[messages.biometric_failed.id],
expect.any(Array),
{cancelable: false},
);
@ -495,8 +497,8 @@ describe('SecurityManager', () => {
expect(result).toBe(true);
await TestHelper.wait(300);
expect(Alert.alert).toHaveBeenCalledWith(
translations[t('mobile.managed.blocked_by')].replace('{vendor}', siteName),
translations[t('mobile.managed.jailbreak')].replace('{vendor}', siteName),
translations[messages.blocked_by.id].replace('{vendor}', siteName),
translations[messages.jailbreak.id].replace('{vendor}', siteName),
expect.any(Array),
{cancelable: false},
);

View file

@ -513,3 +513,7 @@ class SecurityManagerSingleton {
const SecurityManager = new SecurityManagerSingleton();
export default SecurityManager;
export const exportsForTesting = {
messages,
};

View file

@ -811,7 +811,7 @@ describe('Actions.Calls', () => {
expect(Alert.alert).toHaveBeenCalledWith(
'Error',
'You don\'t have permission to end the call. Please ask the call owner to end the call.',
'You don\'t have permission to end the call. Please ask the call host to end the call.',
);
expect(mockClient.endCall).not.toHaveBeenCalled();
});

View file

@ -399,8 +399,8 @@ export const getEndCallMessage = async (serverUrl: string, channelId: string, cu
msg = intl.formatMessage({
id: 'mobile.calls_end_msg_channel',
defaultMessage: 'Are you sure you want to end a call with {numSessions} participants in {displayName}?',
}, {numSessions, displayName: channel.displayName});
defaultMessage: 'Are you sure you want to end a call with {numParticipants} participants in {displayName}?',
}, {numParticipants: numSessions, displayName: channel.displayName});
if (channel.type === General.DM_CHANNEL) {
const otherID = getUserIdFromChannelName(currentUserId, channel.name);
@ -615,7 +615,7 @@ const handleEndCall = async (serverUrl: string, channelId: string, currentUserId
}),
intl.formatMessage({
id: 'mobile.calls_end_permission_msg',
defaultMessage: 'You don\'t have permission to end the call. Please ask the call owner to end the call.',
defaultMessage: 'You don\'t have permission to end the call. Please ask the call host to end the call.',
}));
return;
}

View file

@ -126,7 +126,7 @@ describe('alerts', () => {
expect(mockAlert).toHaveBeenCalledWith(
'You are recording',
'Consider letting everyone know that this meeting is being recorded.',
'You are recording this meeting. Consider letting everyone know that this meeting is being recorded.',
[{
text: 'Dismiss',
}],
@ -139,7 +139,7 @@ describe('alerts', () => {
expect(mockAlert).toHaveBeenCalledWith(
'You are recording',
'Consider letting everyone know that this meeting is being recorded.',
'You are recording this meeting. Consider letting everyone know that this meeting is being recorded.',
[{
text: 'Dismiss',
}],

View file

@ -115,7 +115,7 @@ export const leaveAndJoinWithAlert = async (
}, {leaveChannelName, joinChannelName});
if (newCall) {
joinMessage = formatMessage({
id: 'mobile.leave_and_join_message',
id: 'mobile.leave_and_create_message',
defaultMessage: 'You are already on a channel call in ~{leaveChannelName}. Do you want to leave your current call and start a new call in ~{joinChannelName}?',
}, {leaveChannelName, joinChannelName});
}
@ -268,7 +268,7 @@ export const recordingAlert = (isHost: boolean, transcriptionsEnabled: boolean,
});
const hostMessage = formatMessage({
id: 'mobile.calls_host_rec',
defaultMessage: 'Consider letting everyone know that this meeting is being recorded.',
defaultMessage: 'You are recording this meeting. Consider letting everyone know that this meeting is being recorded.',
});
const participantTitle = formatMessage({

View file

@ -8,6 +8,8 @@ import {Pressable, type StyleProp, Text, type TextStyle, type ViewStyle} from 'r
import {setSpeakerphoneOn} from '@calls/actions';
import CompassIcon from '@components/compass_icon';
import {messages} from './messages';
import type {CurrentCall} from '@calls/types/calls';
type Props = {
@ -19,7 +21,7 @@ type Props = {
export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, currentCall}: Props) => {
const intl = useIntl();
const speakerLabel = intl.formatMessage({id: 'mobile.calls_speaker', defaultMessage: 'SpeakerPhone'});
const speakerLabel = intl.formatMessage(messages.speaker);
const toggleSpeakerPhone = useCallback(() => {
setSpeakerphoneOn(!currentCall?.speakerphoneOn);

View file

@ -15,6 +15,8 @@ import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {messages} from './messages';
type Props = {
pressableStyle: StyleProp<ViewStyle>;
iconStyle: StyleProp<TextStyle>;
@ -36,7 +38,7 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c
const audioDeviceInfo = currentCall.audioDeviceInfo;
const phoneLabel = intl.formatMessage({id: 'mobile.calls_phone', defaultMessage: 'Phone'});
const tabletLabel = intl.formatMessage({id: 'mobile.calls_tablet', defaultMessage: 'Tablet'});
const speakerLabel = intl.formatMessage({id: 'mobile.calls_speaker', defaultMessage: 'SpeakerPhone'});
const speakerLabel = intl.formatMessage(messages.speaker);
const bluetoothLabel = intl.formatMessage({id: 'mobile.calls_bluetooth', defaultMessage: 'Bluetooth'});
const headsetLabel = intl.formatMessage({id: 'mobile.calls_headset', defaultMessage: 'Headset'});

View file

@ -9,7 +9,7 @@ import {useTryCallsFunction} from '@calls/hooks';
import {getCallsConfig} from '@calls/state';
import OptionItem from '@components/option_item';
import {useServerUrl} from '@context/server';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
interface Props {
channelId: string;
@ -34,9 +34,11 @@ const ChannelInfoEnableCalls = ({channelId, enabled}: Props) => {
return null;
}
const onPress = usePreventDoubleTap(tryOnPress);
return (
<OptionItem
action={preventDoubleTap(tryOnPress)}
action={onPress}
label={(enabled ? disableText : enableText) + msgPostfix}
icon='phone'
type='default'

View file

@ -10,7 +10,7 @@ import {useTryCallsFunction} from '@calls/hooks';
import Loading from '@components/loading';
import OptionBox, {OPTIONS_HEIGHT} from '@components/option_box';
import {useTheme} from '@context/theme';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -84,9 +84,24 @@ const ChannelInfoStartButton = ({
setConnecting(false);
dismissChannelInfo();
}
}, [isLimitRestricted, alreadyInCall, dismissChannelInfo, intl, serverUrl, channelId, isACallInCurrentChannel, otherParticipants]);
}, [
alreadyInCall,
isLimitRestricted,
intl,
otherParticipants,
isAdmin,
isHost,
serverUrl,
channelId,
dismissChannelInfo,
limitRestrictedInfo,
isACallInCurrentChannel,
joining,
starting,
]);
const [tryJoin, msgPostfix] = useTryCallsFunction(toggleJoinLeave);
const onPress = usePreventDoubleTap(tryJoin);
const joinText = intl.formatMessage({id: 'mobile.calls_join_call', defaultMessage: 'Join call'});
const startText = intl.formatMessage({id: 'mobile.calls_start_call', defaultMessage: 'Start call'});
@ -108,7 +123,7 @@ const ChannelInfoStartButton = ({
return (
<OptionBox
onPress={preventDoubleTap(tryJoin)}
onPress={onPress}
text={text}
iconName={icon}
activeText={text}

View file

@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {defineMessages} from 'react-intl';
export const messages = defineMessages({
speaker: {
id: 'mobile.calls_speaker',
defaultMessage: 'Speaker',
},
});

View file

@ -236,7 +236,7 @@ function AppsFormComponent({
case AppCallResponseTypes.NAVIGATE:
updateErrors([], undefined, intl.formatMessage({
id: 'apps.error.responses.unexpected_type',
defaultMessage: 'App response type was not expected. Response type: {type}.',
defaultMessage: 'App response type was not expected. Response type: {type}',
}, {
type: callResponse.type,
}));
@ -340,7 +340,7 @@ function AppsFormComponent({
const errorResponse = res.error;
const errMsg = errorResponse.text || intl.formatMessage({
id: 'apps.error.unknown',
defaultMessage: 'Unknown error.',
defaultMessage: 'Unknown error occurred.',
});
setErrors({[field.name]: errMsg});
return [];
@ -357,7 +357,7 @@ function AppsFormComponent({
case AppCallResponseTypes.NAVIGATE: {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.unexpected_type',
defaultMessage: 'App response type was not expected. Response type: {type}.',
defaultMessage: 'App response type was not expected. Response type: {type}',
}, {
type: callResp.type,
},

View file

@ -151,7 +151,7 @@ function AppsFormContainer({
case AppCallResponseTypes.NAVIGATE:
return {error: makeCallErrorResponse(makeErrorMsg(intl.formatMessage({
id: 'apps.error.responses.unexpected_type',
defaultMessage: 'App response type was not expected. Response type: {type}.',
defaultMessage: 'App response type was not expected. Response type: {type}',
}, {
type: callResp.type,
},

View file

@ -2,14 +2,13 @@
// See LICENSE.txt for license information.
import React, {useEffect, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import {fetchChannelCreator} from '@actions/remote/channel';
import CompassIcon from '@components/compass_icon';
import {General, Permissions} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {hasPermission} from '@utils/role';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -55,6 +54,33 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const messages = defineMessages({
publicChannel: {
id: 'intro.public_channel',
defaultMessage: 'Public Channel',
},
privateChannel: {
id: 'intro.private_channel',
defaultMessage: 'Private Channel',
},
welcomePublic: {
id: 'intro.welcome.public',
defaultMessage: 'Add some more team members to the channel or start a conversation below.',
},
welcomePrivate: {
id: 'intro.welcome.private',
defaultMessage: 'Only invited members can see messages posted in this private channel.',
},
welcome: {
id: 'intro.welcome',
defaultMessage: 'Welcome to {displayName} channel.',
},
createdBy: {
id: 'intro.created_by',
defaultMessage: 'created by {creator} on {date}.',
},
});
const PublicOrPrivateChannel = ({channel, creator, roles, theme}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
@ -90,35 +116,30 @@ const PublicOrPrivateChannel = ({channel, creator, roles, theme}: Props) => {
}, [channel.type, roles, channel.deleteAt]);
const createdBy = useMemo(() => {
const id = channel.type === General.OPEN_CHANNEL ? t('intro.public_channel') : t('intro.private_channel');
const defaultMessage = channel.type === General.OPEN_CHANNEL ? 'Public Channel' : 'Private Channel';
const channelType = `${intl.formatMessage({id, defaultMessage})} `;
const message = channel.type === General.OPEN_CHANNEL ? messages.publicChannel : messages.privateChannel;
const channelType = `${intl.formatMessage(message)} `;
const date = intl.formatDate(channel.createAt, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const by = intl.formatMessage({id: 'intro.created_by', defaultMessage: 'created by {creator} on {date}.'}, {
const by = intl.formatMessage(messages.createdBy, {
creator,
date,
});
return `${channelType} ${by}`;
}, [channel.type, creator, theme]);
}, [channel.createAt, channel.type, creator, intl]);
const message = useMemo(() => {
const id = channel.type === General.OPEN_CHANNEL ? t('intro.welcome.public') : t('intro.welcome.private');
const msg = channel.type === General.OPEN_CHANNEL ? 'Add some more team members to the channel or start a conversation below.' : 'Only invited members can see messages posted in this private channel.';
const mainMessage = intl.formatMessage({
id: 'intro.welcome',
defaultMessage: 'Welcome to {displayName} channel.',
}, {displayName: channel.displayName});
const msg = channel.type === General.OPEN_CHANNEL ? messages.welcomePublic : messages.welcomePrivate;
const mainMessage = intl.formatMessage(messages.welcome, {displayName: channel.displayName});
const suffix = intl.formatMessage({id, defaultMessage: msg});
const suffix = intl.formatMessage(msg);
return `${mainMessage} ${suffix}`;
}, [channel.displayName, channel.type, theme]);
}, [channel.displayName, channel.type, intl]);
return (
<View style={styles.container}>

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {defineMessage, useIntl} from 'react-intl';
import {Keyboard, type LayoutChangeEvent, Platform, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
@ -21,7 +21,6 @@ import {useAccessControlAttributes} from '@hooks/access_control_attributes';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useKeyboardOverlap} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {t} from '@i18n';
import SecurityManager from '@managers/security_manager';
import {dismissModal} from '@screens/navigation';
import {alertErrorWithFallback} from '@utils/draft';
@ -174,7 +173,7 @@ export default function ChannelAddMembers({
const result = await addMembersToChannel(serverUrl, channel.id, idsToUse);
if (result.error) {
alertErrorWithFallback(intl, result.error, {id: t('mobile.channel_add_members.error'), defaultMessage: 'There has been an error and we could not add those users to the channel.'});
alertErrorWithFallback(intl, result.error, defineMessage({id: 'mobile.channel_add_members.error', defaultMessage: 'There has been an error and we could not add those users to the channel.'}));
setAddingMembers(false);
} else {
close();
@ -194,7 +193,7 @@ export default function ChannelAddMembers({
return newSelectedIds;
});
}, [currentUserId, clearSearch]);
}, [clearSearch]);
const onTextChange = useCallback((searchTerm: string) => {
setTerm(searchTerm);

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {defineMessage, useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import Badge from '@components/badge';
@ -9,7 +9,6 @@ import CompassIcon from '@components/compass_icon';
import Filter, {DIVIDERS_HEIGHT, FILTER_ITEM_HEIGHT, FilterData, NUMBER_FILTER_ITEMS} from '@components/files/file_filter';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {t} from '@i18n';
import {TITLE_SEPARATOR_MARGIN, TITLE_SEPARATOR_MARGIN_TABLET, TITLE_HEIGHT} from '@screens/bottom_sheet/content';
import {bottomSheet} from '@screens/navigation';
import {type FileFilter, FileFilters} from '@utils/file';
@ -62,10 +61,10 @@ const Header = ({
const messageObject = hasFilters ? {
id: FilterData[selectedFilter].id,
defaultMessage: FilterData[selectedFilter].defaultMessage,
} : {
id: t('screen.channel_files.header.recent_files'),
} : defineMessage({
id: 'screen.channel_files.header.recent_files',
defaultMessage: 'Recent Files',
};
});
const messagesText = intl.formatMessage(messageObject);
const title = intl.formatMessage({id: 'screen.channel_files.results.filter.title', defaultMessage: 'Filter by file type'});
@ -97,7 +96,7 @@ const Header = ({
theme,
title,
});
}, [onFilterChanged, selectedFilter]);
}, [onFilterChanged, selectedFilter, snapPoints, theme, title]);
return (
<View style={styles.container}>

View file

@ -1,18 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {type MessageDescriptor, useIntl} from 'react-intl';
import React, {useCallback} from 'react';
import {defineMessages, type MessageDescriptor, useIntl} from 'react-intl';
import {Alert} from 'react-native';
import {archiveChannel, unarchiveChannel} from '@actions/remote/channel';
import OptionItem from '@components/option_item';
import {General} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {usePreventDoubleTap} from '@hooks/utils';
import {dismissModal, popToRoot} from '@screens/navigation';
import {alertErrorWithFallback} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
import type {AvailableScreens} from '@typings/screens/navigation';
@ -26,6 +25,54 @@ type Props = {
type?: ChannelType;
}
const messages = defineMessages({
publicChannel: {
id: 'channel_info.public_channel',
defaultMessage: 'Public Channel',
},
privateChannel: {
id: 'channel_info.private_channel',
defaultMessage: 'Private Channel',
},
alertNo: {
id: 'channel_info.alertNo',
defaultMessage: 'No',
},
alertYes: {
id: 'channel_info.alertYes',
defaultMessage: 'Yes',
},
archiveTitle: {
id: 'channel_info.archive_title',
defaultMessage: 'Archive {term}',
},
archiveDescriptionCanViewArchived: {
id: 'channel_info.archive_description.can_view_archived',
defaultMessage: 'This will archive the channel from the team. Channel contents will still be accessible by channel members.\n\nAre you sure you wish to archive the {term} {name}?',
},
archiveDescriptionCannotViewArchived: {
id: 'channel_info.archive_description.cannot_view_archived',
defaultMessage: 'This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again.\n\nAre you sure you wish to archive the {term} {name}?',
},
archiveFailed: {
id: 'channel_info.archive_failed',
defaultMessage: 'An error occurred trying to archive the channel {displayName}',
},
unarchiveTitle: {
id: 'channel_info.unarchive_title',
defaultMessage: 'Unarchive {term}',
},
unarchiveDescription: {
id: 'channel_info.unarchive_description',
defaultMessage: 'Are you sure you want to unarchive the {term} {name}?',
},
unarchiveFailed: {
id: 'channel_info.unarchive_failed',
defaultMessage: 'An error occurred trying to unarchive the channel {displayName}',
},
});
const Archive = ({
canArchive, canUnarchive, canViewArchivedChannels,
channelId, componentId, displayName, type,
@ -33,43 +80,37 @@ const Archive = ({
const intl = useIntl();
const serverUrl = useServerUrl();
const close = async (pop: boolean) => {
const close = useCallback(async (pop: boolean) => {
await dismissModal({componentId});
if (pop) {
popToRoot();
}
};
}, [componentId]);
const alertAndHandleYesAction = (title: MessageDescriptor, message: MessageDescriptor, onPressAction: () => void) => {
const alertAndHandleYesAction = useCallback((title: MessageDescriptor, message: MessageDescriptor, onPressAction: () => void) => {
const {formatMessage} = intl;
let term: string;
if (type === General.OPEN_CHANNEL) {
term = formatMessage({id: 'channel_info.public_channel', defaultMessage: 'Public Channel'});
term = formatMessage(messages.publicChannel);
} else {
term = formatMessage({id: 'channel_info.private_channel', defaultMessage: 'Private Channel'});
term = formatMessage(messages.privateChannel);
}
Alert.alert(
formatMessage(title, {term}),
formatMessage(message, {term: term.toLowerCase(), name: displayName}),
[{
text: formatMessage({id: 'channel_info.alertNo', defaultMessage: 'No'}),
text: formatMessage(messages.alertNo),
}, {
text: formatMessage({id: 'channel_info.alertYes', defaultMessage: 'Yes'}),
text: formatMessage(messages.alertYes),
onPress: onPressAction,
}],
);
};
}, [displayName, intl, type]);
const onArchive = preventDoubleTap(() => {
const title = {id: t('channel_info.archive_title'), defaultMessage: 'Archive {term}'};
const message = canViewArchivedChannels ? {
id: t('channel_info.archive_description.can_view_archived'),
defaultMessage: 'This will archive the channel from the team. Channel contents will still be accessible by channel members.\n\nAre you sure you wish to archive the {term} {name}?',
} : {
id: t('channel_info.archive_description.cannot_view_archived'),
defaultMessage: 'This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again.\n\nAre you sure you wish to archive the {term} {name}?',
};
const onArchive = usePreventDoubleTap(useCallback(() => {
const title = messages.archiveTitle;
const message = canViewArchivedChannels ? messages.archiveDescriptionCanViewArchived : messages.archiveDescriptionCannotViewArchived;
const onPressAction = async () => {
const result = await archiveChannel(serverUrl, channelId);
@ -77,41 +118,34 @@ const Archive = ({
alertErrorWithFallback(
intl,
result.error,
{
id: t('channel_info.archive_failed'),
defaultMessage: 'An error occurred trying to archive the channel {displayName}',
}, {displayName},
messages.archiveFailed,
{displayName},
);
} else {
close(!canViewArchivedChannels);
}
};
alertAndHandleYesAction(title, message, onPressAction);
});
}, [alertAndHandleYesAction, canViewArchivedChannels, channelId, close, displayName, intl, serverUrl]));
const onUnarchive = preventDoubleTap(() => {
const title = {id: t('channel_info.unarchive_title'), defaultMessage: 'Unarchive {term}'};
const message = {
id: t('channel_info.unarchive_description'),
defaultMessage: 'Are you sure you want to unarchive the {term} {name}?',
};
const onUnarchive = usePreventDoubleTap(useCallback(() => {
const title = messages.unarchiveTitle;
const message = messages.unarchiveDescription;
const onPressAction = async () => {
const result = await unarchiveChannel(serverUrl, channelId);
if (result.error) {
alertErrorWithFallback(
intl,
result.error,
{
id: t('channel_info.unarchive_failed'),
defaultMessage: 'An error occurred trying to unarchive the channel {displayName}',
}, {displayName},
messages.unarchiveFailed,
{displayName},
);
} else {
close(false);
}
};
alertAndHandleYesAction(title, message, onPressAction);
});
}, [alertAndHandleYesAction, channelId, close, displayName, intl, serverUrl]));
if (!canArchive && !canUnarchive) {
return null;

View file

@ -1,14 +1,13 @@
// 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 {defineMessages, useIntl, type IntlShape} from 'react-intl';
import {Alert} from 'react-native';
import {convertChannelToPrivate} from '@actions/remote/channel';
import OptionItem from '@components/option_item';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {alertErrorWithFallback} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
@ -18,55 +17,85 @@ type Props = {
displayName: string;
}
const messages = defineMessages({
convertPrivateTitle: {
id: 'channel_info.convert_private_title',
defaultMessage: 'Convert {displayName} to a private channel?',
},
convertPrivateDescription: {
id: 'channel_info.convert_private_description',
defaultMessage: 'When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?',
},
alertNo: {
id: 'channel_info.alertNo',
defaultMessage: 'No',
},
alertYes: {
id: 'channel_info.alertYes',
defaultMessage: 'Yes',
},
convertFailed: {
id: 'channel_info.convert_failed',
defaultMessage: 'We were unable to convert {displayName} to a private channel.',
},
convertPrivateSuccess: {
id: 'channel_info.convert_private_success',
defaultMessage: '{displayName} is now a private channel.',
},
errorClose: {
id: 'channel_info.error_close',
defaultMessage: 'Close',
},
alertRetry: {
id: 'channel_info.alert_retry',
defaultMessage: 'Try Again',
},
});
const convertToPrivate = preventDoubleTap(async (serverUrl: string, channelId: string, displayName: string, intl: IntlShape) => {
const result = await convertChannelToPrivate(serverUrl, channelId);
const {formatMessage} = intl;
if (result.error) {
alertErrorWithFallback(
intl,
result.error,
messages.convertFailed,
{displayName},
[{
text: formatMessage(messages.errorClose),
}, {
text: formatMessage(messages.alertRetry),
onPress: () => convertToPrivate(serverUrl, channelId, displayName, intl),
}],
);
} else {
Alert.alert(
'',
formatMessage(messages.convertPrivateSuccess, {displayName}),
);
}
});
const ConvertPrivate = ({canConvert, channelId, displayName}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const onConfirmConvertToPrivate = () => {
const onConfirmConvertToPrivate = useCallback(() => {
const {formatMessage} = intl;
const title = {id: t('channel_info.convert_private_title'), defaultMessage: 'Convert {displayName} to a private channel?'};
const message = {
id: t('channel_info.convert_private_description'),
defaultMessage: 'When you convert {displayName} to a private channel, history and membership are preserved. Publicly shared files remain accessible to anyone with the link. Membership in a private channel is by invitation only.\n\nThe change is permanent and cannot be undone.\n\nAre you sure you want to convert {displayName} to a private channel?',
};
const title = messages.convertPrivateTitle;
const message = messages.convertPrivateDescription;
Alert.alert(
formatMessage(title, {displayName}),
formatMessage(message, {displayName}),
[{
text: formatMessage({id: 'channel_info.alertNo', defaultMessage: 'No'}),
text: formatMessage(messages.alertNo),
}, {
text: formatMessage({id: 'channel_info.alertYes', defaultMessage: 'Yes'}),
onPress: convertToPrivate,
text: formatMessage(messages.alertYes),
onPress: () => convertToPrivate(serverUrl, channelId, displayName, intl),
}],
);
};
const convertToPrivate = preventDoubleTap(async () => {
const result = await convertChannelToPrivate(serverUrl, channelId);
const {formatMessage} = intl;
if (result.error) {
alertErrorWithFallback(
intl,
result.error,
{
id: t('channel_info.convert_failed'),
defaultMessage: 'We were unable to convert {displayName} to a private channel.',
}, {displayName},
[{
text: formatMessage({id: 'channel_info.error_close', defaultMessage: 'Close'}),
}, {
text: formatMessage({id: 'channel_info.alert_retry', defaultMessage: 'Try Again'}),
onPress: convertToPrivate,
}],
);
} else {
Alert.alert(
'',
formatMessage({id: t('channel_info.convert_private_success'), defaultMessage: '{displayName} is now a private channel.'}, {displayName}),
);
}
});
}, [channelId, displayName, intl, serverUrl]);
if (!canConvert) {
return null;

View file

@ -1,16 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Platform} from 'react-native';
import OptionItem from '@components/option_item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {usePreventDoubleTap} from '@hooks/utils';
import {getHeaderOptions} from '@screens/channel_add_members/channel_add_members';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
type Props = {
channelId: string;
@ -22,10 +22,10 @@ const AddMembers = ({displayName, channelId}: Props) => {
const theme = useTheme();
const title = formatMessage({id: 'channel_info.add_members', defaultMessage: 'Add members'});
const goToAddMembers = preventDoubleTap(async () => {
const goToAddMembers = usePreventDoubleTap(useCallback(async () => {
const options = await getHeaderOptions(theme, displayName, true);
goToScreen(Screens.CHANNEL_ADD_MEMBERS, title, {channelId, inModal: true}, options);
});
}, [channelId, displayName, theme, title]));
return (
<OptionItem

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import React, {useCallback, useState} from 'react';
import {defineMessage, useIntl} from 'react-intl';
import {updateChannelNotifyProps} from '@actions/remote/channel';
@ -11,8 +11,8 @@ import {
CHANNEL_AUTO_FOLLOW_THREADS_TRUE,
} from '@constants/channel';
import {useServerUrl} from '@context/server';
import {usePreventDoubleTap} from '@hooks/utils';
import {alertErrorWithFallback} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
type Props = {
channelId: string;
@ -25,7 +25,7 @@ const AutoFollowThreads = ({channelId, displayName, followedStatus}: Props) => {
const serverUrl = useServerUrl();
const intl = useIntl();
const toggleFollow = preventDoubleTap(async () => {
const toggleFollow = usePreventDoubleTap(useCallback(async () => {
const props: Partial<ChannelNotifyProps> = {
channel_auto_follow_threads: followedStatus ? CHANNEL_AUTO_FOLLOW_THREADS_FALSE : CHANNEL_AUTO_FOLLOW_THREADS_TRUE,
};
@ -43,7 +43,7 @@ const AutoFollowThreads = ({channelId, displayName, followedStatus}: Props) => {
);
setAutoFollow((v) => !v);
}
});
}, [channelId, displayName, followedStatus, intl, serverUrl]));
return (
<OptionItem

View file

@ -1,15 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Platform} from 'react-native';
import OptionItem from '@components/option_item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {usePreventDoubleTap} from '@hooks/utils';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity} from '@utils/theme';
type Props = {
@ -23,7 +23,7 @@ const ChannelFiles = ({channelId, count, displayName}: Props) => {
const {formatMessage} = useIntl();
const title = formatMessage({id: 'channel_info.channel_files', defaultMessage: 'Files'});
const goToChannelFiles = preventDoubleTap(() => {
const goToChannelFiles = usePreventDoubleTap(useCallback(() => {
const options = {
topBar: {
title: {
@ -36,7 +36,7 @@ const ChannelFiles = ({channelId, count, displayName}: Props) => {
},
};
goToScreen(Screens.CHANNEL_FILES, title, {channelId}, options);
});
}, [channelId, displayName, theme.sidebarHeaderTextColor, title]));
return (
<OptionItem

View file

@ -1,14 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Platform} from 'react-native';
import OptionItem from '@components/option_item';
import {Screens} from '@constants';
import {usePreventDoubleTap} from '@hooks/utils';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
type Props = {
channelId: string;
@ -18,9 +18,9 @@ const EditChannel = ({channelId}: Props) => {
const {formatMessage} = useIntl();
const title = formatMessage({id: 'screens.channel_edit', defaultMessage: 'Edit Channel'});
const goToEditChannel = preventDoubleTap(async () => {
const goToEditChannel = usePreventDoubleTap(useCallback(async () => {
goToScreen(Screens.CREATE_OR_EDIT_CHANNEL, title, {channelId});
});
}, [channelId, title]));
return (
<OptionItem

View file

@ -1,14 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import React, {useCallback, useState} from 'react';
import {defineMessage, useIntl} from 'react-intl';
import {updateChannelNotifyProps} from '@actions/remote/channel';
import OptionItem from '@components/option_item';
import {useServerUrl} from '@context/server';
import {usePreventDoubleTap} from '@hooks/utils';
import {alertErrorWithFallback} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
type Props = {
channelId: string;
@ -21,7 +21,7 @@ const IgnoreMentions = ({channelId, ignoring, displayName}: Props) => {
const serverUrl = useServerUrl();
const intl = useIntl();
const toggleIgnore = preventDoubleTap(async () => {
const toggleIgnore = usePreventDoubleTap(useCallback(async () => {
const props: Partial<ChannelNotifyProps> = {
ignore_channel_mentions: ignoring ? 'off' : 'on',
};
@ -39,7 +39,7 @@ const IgnoreMentions = ({channelId, ignoring, displayName}: Props) => {
);
setIgnored((v) => !v);
}
});
}, [channelId, displayName, ignoring, intl, serverUrl]));
return (
<OptionItem

View file

@ -1,15 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Platform} from 'react-native';
import OptionItem from '@components/option_item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {usePreventDoubleTap} from '@hooks/utils';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity} from '@utils/theme';
type Props = {
@ -22,7 +22,7 @@ const Members = ({displayName, channelId, count}: Props) => {
const {formatMessage} = useIntl();
const theme = useTheme();
const title = formatMessage({id: 'channel_info.members', defaultMessage: 'Members'});
const goToChannelMembers = preventDoubleTap(() => {
const goToChannelMembers = usePreventDoubleTap(useCallback(() => {
const options = {
topBar: {
subtitle: {
@ -33,7 +33,7 @@ const Members = ({displayName, channelId, count}: Props) => {
};
goToScreen(Screens.MANAGE_CHANNEL_MEMBERS, title, {channelId}, options);
});
}, [channelId, displayName, theme.sidebarHeaderTextColor, title]));
return (
<OptionItem

View file

@ -1,17 +1,16 @@
// 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 {defineMessages, useIntl} from 'react-intl';
import {Platform} from 'react-native';
import OptionItem from '@components/option_item';
import {NotificationLevel, Screens} from '@constants';
import {useTheme} from '@context/theme';
import {t} from '@i18n';
import {usePreventDoubleTap} from '@hooks/utils';
import {goToScreen} from '@screens/navigation';
import {isTypeDMorGM} from '@utils/channel';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity} from '@utils/theme';
import type {Options} from 'react-native-navigation';
@ -25,32 +24,46 @@ type Props = {
hasGMasDMFeature: boolean;
}
const messages = defineMessages({
notificationAll: {
id: 'channel_info.notification.all',
defaultMessage: 'All',
},
notificationMention: {
id: 'channel_info.notification.mention',
defaultMessage: 'Mentions',
},
notificationNone: {
id: 'channel_info.notification.none',
defaultMessage: 'Never',
},
notificationDefault: {
id: 'channel_info.notification.default',
defaultMessage: 'Default',
},
});
const notificationLevel = (notifyLevel: NotificationLevel) => {
let id = '';
let defaultMessage = '';
let message;
switch (notifyLevel) {
case NotificationLevel.ALL: {
id = t('channel_info.notification.all');
defaultMessage = 'All';
message = messages.notificationAll;
break;
}
case NotificationLevel.MENTION: {
id = t('channel_info.notification.mention');
defaultMessage = 'Mentions';
message = messages.notificationMention;
break;
}
case NotificationLevel.NONE: {
id = t('channel_info.notification.none');
defaultMessage = 'Never';
message = messages.notificationNone;
break;
}
default:
id = t('channel_info.notification.default');
defaultMessage = 'Default';
message = messages.notificationDefault;
break;
}
return {id, defaultMessage};
return message;
};
const NotificationPreference = ({
@ -65,7 +78,7 @@ const NotificationPreference = ({
const theme = useTheme();
const title = formatMessage({id: 'channel_info.mobile_notifications', defaultMessage: 'Mobile Notifications'});
const goToChannelNotificationPreferences = preventDoubleTap(() => {
const goToChannelNotificationPreferences = usePreventDoubleTap(useCallback(() => {
const options: Options = {
topBar: {
title: {
@ -81,7 +94,7 @@ const NotificationPreference = ({
},
};
goToScreen(Screens.CHANNEL_NOTIFICATION_PREFERENCES, title, {channelId}, options);
});
}, [channelId, displayName, theme.sidebarHeaderTextColor, title]));
const notificationLevelToText = () => {
let notifyLevelToUse = notifyLevel;

View file

@ -1,15 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {Platform} from 'react-native';
import OptionItem from '@components/option_item';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {usePreventDoubleTap} from '@hooks/utils';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity} from '@utils/theme';
type Props = {
@ -23,7 +23,7 @@ const PinnedMessages = ({channelId, count, displayName}: Props) => {
const {formatMessage} = useIntl();
const title = formatMessage({id: 'channel_info.pinned_messages', defaultMessage: 'Pinned Messages'});
const goToPinnedMessages = preventDoubleTap(() => {
const goToPinnedMessages = usePreventDoubleTap(useCallback(() => {
const options = {
topBar: {
title: {
@ -36,7 +36,7 @@ const PinnedMessages = ({channelId, count, displayName}: Props) => {
},
};
goToScreen(Screens.PINNED_MESSAGES, title, {channelId}, options);
});
}, [channelId, displayName, theme.sidebarHeaderTextColor, title]));
return (
<OptionItem

View file

@ -2,14 +2,13 @@
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {type LayoutChangeEvent, View} from 'react-native';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {NotificationLevel} from '@constants';
import {t} from '@i18n';
import type {SharedValue} from 'react-native-reanimated';
@ -30,24 +29,40 @@ type NotifPrefOptions = {
export const BLOCK_TITLE_HEIGHT = 13;
const NOTIFY_ABOUT = {id: t('channel_notification_preferences.notify_about'), defaultMessage: 'Notify me about...'};
const messages = defineMessages({
notifyAbout: {
id: 'channel_notification_preferences.notify_about',
defaultMessage: 'Notify me about...',
},
[NotificationLevel.ALL]: {
id: 'channel_notification_preferences.notification.all',
defaultMessage: 'All new messages',
},
[NotificationLevel.MENTION]: {
id: 'channel_notification_preferences.notification.mention',
defaultMessage: 'Mentions only',
},
[NotificationLevel.NONE]: {
id: 'channel_notification_preferences.notification.none',
defaultMessage: 'Nothing',
},
});
const NOTIFY_ABOUT = messages.notifyAbout;
const NOTIFY_OPTIONS: Record<string, NotifPrefOptions> = {
[NotificationLevel.ALL]: {
defaultMessage: 'All new messages',
id: t('channel_notification_preferences.notification.all'),
...messages[NotificationLevel.ALL],
testID: 'channel_notification_preferences.notification.all',
value: NotificationLevel.ALL,
},
[NotificationLevel.MENTION]: {
defaultMessage: 'Mentions only',
id: t('channel_notification_preferences.notification.mention'),
...messages[NotificationLevel.MENTION],
testID: 'channel_notification_preferences.notification.mention',
value: NotificationLevel.MENTION,
},
[NotificationLevel.NONE]: {
defaultMessage: 'Nothing',
id: t('channel_notification_preferences.notification.none'),
...messages[NotificationLevel.NONE],
testID: 'channel_notification_preferences.notification.none',
value: NotificationLevel.NONE,
},

View file

@ -2,13 +2,12 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import SettingBlock from '@components/settings/block';
import SettingOption from '@components/settings/option';
import SettingSeparator from '@components/settings/separator';
import {NotificationLevel} from '@constants';
import {t} from '@i18n';
type Props = {
isSelected: boolean;
@ -22,12 +21,21 @@ type NotifPrefOptions = {
testID: string;
value: string;
}
const messages = defineMessages({
threadReplies: {
id: 'channel_notification_preferences.thread_replies',
defaultMessage: 'Thread replies',
},
threadRepliesDescription: {
id: 'channel_notification_preferences.notification.thread_replies',
defaultMessage: 'Notify me about replies to threads Im following in this channel',
},
});
const THREAD_REPLIES = {id: t('channel_notification_preferences.thread_replies'), defaultMessage: 'Thread replies'};
const THREAD_REPLIES = messages.threadReplies;
const NOTIFY_OPTIONS_THREAD: Record<string, NotifPrefOptions> = {
THREAD_REPLIES: {
defaultMessage: 'Notify me about replies to threads Im following in this channel',
id: t('channel_notification_preferences.notification.thread_replies'),
...messages.threadRepliesDescription,
testID: 'channel_notification_preferences.notification.thread_replies',
value: 'thread_replies',
},

View file

@ -10,9 +10,9 @@ import Button from '@components/button';
import {ServerErrors} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {usePreventDoubleTap} from '@hooks/utils';
import {isErrorWithMessage, isServerError} from '@utils/errors';
import {logError} from '@utils/log';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {displayUsername} from '@utils/user';
@ -70,7 +70,7 @@ export const ConvertGMToChannelForm = ({
const userDisplayNames = useMemo(() => profiles.map((profile) => displayUsername(profile, locale, teammateNameDisplay)), [profiles, teammateNameDisplay, locale]);
const submitButtonEnabled = !conversionInProgress && selectedTeam && newChannelName.trim();
const handleOnPress = useCallback(preventDoubleTap(async () => {
const handleOnPress = usePreventDoubleTap(useCallback(async () => {
if (!submitButtonEnabled) {
return;
}
@ -101,7 +101,7 @@ export const ConvertGMToChannelForm = ({
setErrorMessage('');
switchToChannelById(serverUrl, updatedChannel.id, selectedTeam.id);
setConversionInProgress(false);
}), [selectedTeam, newChannelName, submitButtonEnabled]);
}, [submitButtonEnabled, serverUrl, channelId, selectedTeam.id, newChannelName, formatMessage]));
if (commonTeams.length === 0) {
return (

View file

@ -27,7 +27,6 @@ import {useTheme} from '@context/theme';
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
import {useKeyboardHeight, useKeyboardOverlap} from '@hooks/device';
import {useInputPropagation} from '@hooks/input';
import {t} from '@i18n';
import {
changeOpacity,
makeStyleSheetFromTheme,
@ -133,17 +132,17 @@ export default function ChannelInfoForm({
const [headerFieldHeight, setHeaderFieldHeight] = useState(0);
const [headerPosition, setHeaderPosition] = useState(0);
const optionalText = formatMessage({id: t('channel_modal.optional'), defaultMessage: '(optional)'});
const labelDisplayName = formatMessage({id: t('channel_modal.name'), defaultMessage: 'Name'});
const labelPurpose = formatMessage({id: t('channel_modal.purpose'), defaultMessage: 'Purpose'}) + ' ' + optionalText;
const labelHeader = formatMessage({id: t('channel_modal.header'), defaultMessage: 'Header'}) + ' ' + optionalText;
const optionalText = formatMessage({id: 'channel_modal.optional', defaultMessage: '(optional)'});
const labelDisplayName = formatMessage({id: 'channel_modal.name', defaultMessage: 'Name'});
const labelPurpose = formatMessage({id: 'channel_modal.purpose', defaultMessage: 'Purpose'}) + ' ' + optionalText;
const labelHeader = formatMessage({id: 'channel_modal.header', defaultMessage: 'Header'}) + ' ' + optionalText;
const placeholderDisplayName = formatMessage({id: t('channel_modal.nameEx'), defaultMessage: 'Bugs, Marketing'});
const placeholderPurpose = formatMessage({id: t('channel_modal.purposeEx'), defaultMessage: 'A channel to file bugs and improvements'});
const placeholderHeader = formatMessage({id: t('channel_modal.headerEx'), defaultMessage: 'Use Markdown to format header text'});
const placeholderDisplayName = formatMessage({id: 'channel_modal.nameEx', defaultMessage: 'Bugs, Marketing'});
const placeholderPurpose = formatMessage({id: 'channel_modal.purposeEx', defaultMessage: 'A channel to file bugs and improvements'});
const placeholderHeader = formatMessage({id: 'channel_modal.headerEx', defaultMessage: 'Use Markdown to format header text'});
const makePrivateLabel = formatMessage({id: t('channel_modal.makePrivate.label'), defaultMessage: 'Make Private'});
const makePrivateDescription = formatMessage({id: t('channel_modal.makePrivate.description'), defaultMessage: 'When a channel is set to private, only invited team members can access and participate in that channel.'});
const makePrivateLabel = formatMessage({id: 'channel_modal.makePrivate.label', defaultMessage: 'Make Private'});
const makePrivateDescription = formatMessage({id: 'channel_modal.makePrivate.description', defaultMessage: 'When a channel is set to private, only invited team members can access and participate in that channel'});
const displayHeaderOnly = headerOnly || channelType === General.DM_CHANNEL || channelType === General.GM_CHANNEL;
const showSelector = !displayHeaderOnly && !editing;

View file

@ -9,7 +9,7 @@ import ClearButton from '@components/custom_status/clear_button';
import CustomStatusText from '@components/custom_status/custom_status_text';
import Emoji from '@components/emoji';
import {CST} from '@constants/custom_status';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -67,15 +67,15 @@ const CustomStatusSuggestion = ({duration, emoji, expires_at, handleClear, handl
const style = getStyleSheet(theme);
const intl = useIntl();
const handleClick = useCallback(preventDoubleTap(() => {
const handleClick = usePreventDoubleTap(useCallback(() => {
handleSuggestionClick({emoji, text, duration});
}), []);
}, [duration, emoji, handleSuggestionClick, text]));
const handleSuggestionClear = useCallback(() => {
if (handleClear) {
handleClear({emoji, text, duration, expires_at});
}
}, []);
}, [duration, emoji, expires_at, handleClear, text]);
const showCustomStatus = Boolean(duration && duration !== 'date_and_time' && isExpirySupported);
const customStatusSuggestionTestId = `custom_status.custom_status_suggestion.${text}`;

View file

@ -2,16 +2,14 @@
// See LICENSE.txt for license information.
import React from 'react';
import {defineMessages, type IntlShape, type MessageDescriptor} from 'react-intl';
import {View} from 'react-native';
import FormattedText from '@components/formatted_text';
import {t} from '@i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import CustomStatusSuggestion from './custom_status_suggestion';
import type {IntlShape} from 'react-intl';
type Props = {
intl: IntlShape;
onHandleCustomStatusSuggestionClick: (status: UserCustomStatus) => void;
@ -21,8 +19,7 @@ type Props = {
type DefaultUserCustomStatus = {
emoji: string;
message: string;
messageDefault: string;
message: MessageDescriptor;
durationDefault: CustomStatusDuration;
};
@ -47,12 +44,35 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
const messages = defineMessages({
inAMeeting: {
id: 'custom_status.suggestions.in_a_meeting',
defaultMessage: 'In a meeting',
},
outForLunch: {
id: 'custom_status.suggestions.out_for_lunch',
defaultMessage: 'Out for lunch',
},
outSick: {
id: 'custom_status.suggestions.out_sick',
defaultMessage: 'Out sick',
},
workingFromHome: {
id: 'custom_status.suggestions.working_from_home',
defaultMessage: 'Working from home',
},
onAVacation: {
id: 'custom_status.suggestions.on_a_vacation',
defaultMessage: 'On a vacation',
},
});
const defaultCustomStatusSuggestions: DefaultUserCustomStatus[] = [
{emoji: 'calendar', message: t('custom_status.suggestions.in_a_meeting'), messageDefault: 'In a meeting', durationDefault: 'one_hour'},
{emoji: 'hamburger', message: t('custom_status.suggestions.out_for_lunch'), messageDefault: 'Out for lunch', durationDefault: 'thirty_minutes'},
{emoji: 'sneezing_face', message: t('custom_status.suggestions.out_sick'), messageDefault: 'Out sick', durationDefault: 'today'},
{emoji: 'house', message: t('custom_status.suggestions.working_from_home'), messageDefault: 'Working from home', durationDefault: 'today'},
{emoji: 'palm_tree', message: t('custom_status.suggestions.on_a_vacation'), messageDefault: 'On a vacation', durationDefault: 'this_week'},
{emoji: 'calendar', message: messages.inAMeeting, durationDefault: 'one_hour'},
{emoji: 'hamburger', message: messages.outForLunch, durationDefault: 'thirty_minutes'},
{emoji: 'sneezing_face', message: messages.outSick, durationDefault: 'today'},
{emoji: 'house', message: messages.workingFromHome, durationDefault: 'today'},
{emoji: 'palm_tree', message: messages.onAVacation, durationDefault: 'this_week'},
];
const CustomStatusSuggestions = ({
@ -67,7 +87,7 @@ const CustomStatusSuggestions = ({
const customStatusSuggestions = defaultCustomStatusSuggestions.
map((status) => ({
emoji: status.emoji,
text: intl.formatMessage({id: status.message, defaultMessage: status.messageDefault}),
text: intl.formatMessage(status.message),
duration: status.durationDefault,
})).
filter((status) => !recentCustomStatusTexts.has(status.text)).
@ -92,7 +112,7 @@ const CustomStatusSuggestions = ({
<View style={style.separator}/>
<View testID='custom_status.suggestions'>
<FormattedText
id={t('custom_status.suggestions.title')}
id={'custom_status.suggestions.title'}
defaultMessage='Suggestions'
style={style.title}
/>

View file

@ -5,7 +5,6 @@ import React from 'react';
import {View} from 'react-native';
import FormattedText from '@components/formatted_text';
import {t} from '@i18n';
import CustomStatusSuggestion from '@screens/custom_status/components/custom_status_suggestion';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -49,7 +48,7 @@ const RecentCustomStatuses = ({onHandleClear, onHandleSuggestionClick, recentCus
<View style={style.separator}/>
<View testID='custom_status.recents'>
<FormattedText
id={t('custom_status.suggestions.recent_title')}
id={'custom_status.suggestions.recent_title'}
defaultMessage='Recent'
style={style.title}
/>

View file

@ -19,12 +19,12 @@ import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {usePreventDoubleTap} from '@hooks/utils';
import SecurityManager from '@managers/security_manager';
import {dismissModal, goToScreen, openAsBottomSheet, showModal} from '@screens/navigation';
import {getCurrentMomentForTimezone, getRoundedTime} from '@utils/helpers';
import {logDebug} from '@utils/log';
import {mergeNavigationOptions} from '@utils/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {
getTimezone,
@ -304,9 +304,18 @@ const CustomStatus = ({
}
}
dismissModalAndKeyboard(isTablet, {componentId});
}, [newStatus, isStatusSet, storedStatus, currentUser, isTablet]);
}, [
currentUser,
isStatusSet,
storedStatus,
isTablet,
componentId,
newStatus,
customStatusExpirySupported,
serverUrl,
]);
const openEmojiPicker = useCallback(preventDoubleTap(() => {
const openEmojiPicker = usePreventDoubleTap(useCallback(() => {
openAsBottomSheet({
closeButtonId: 'close-emoji-picker',
screen: Screens.EMOJI_PICKER,
@ -314,7 +323,7 @@ const CustomStatus = ({
title: intl.formatMessage({id: 'mobile.custom_status.choose_emoji', defaultMessage: 'Choose an emoji'}),
props: {onEmojiPress: handleEmojiClick},
});
}), [theme, intl, handleEmojiClick]);
}, [theme, intl, handleEmojiClick]));
const handleBackButton = useCallback(() => {
dismissModalAndKeyboard(isTablet, {componentId});

View file

@ -12,7 +12,7 @@ import CustomStatusText from '@components/custom_status/custom_status_text';
import DateTimePicker from '@components/data_time_selector';
import {CST, CustomStatusDurationEnum} from '@constants/custom_status';
import {useTheme} from '@context/theme';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {getTimezone} from '@utils/user';
@ -82,9 +82,9 @@ const ClearAfterMenuItem = ({currentUser, duration, expiryTime = '', handleItemC
[CustomStatusDurationEnum.DATE_AND_TIME]: intl.formatMessage({id: 'custom_status.expiry_dropdown.custom', defaultMessage: 'Custom'}),
};
const handleClick = preventDoubleTap(() => {
const handleClick = usePreventDoubleTap(useCallback(() => {
handleItemClick(duration, expiryTime);
});
}, [duration, expiryTime, handleItemClick]));
const handleCustomExpiresAtChange = useCallback((expiresAt: Moment) => {
handleItemClick(duration, expiresAt.toISOString());

View file

@ -2,12 +2,11 @@
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {type MessageDescriptor, useIntl} from 'react-intl';
import {defineMessages, type MessageDescriptor, useIntl} from 'react-intl';
import {Keyboard, StyleSheet, View} from 'react-native';
import {useTheme} from '@context/theme';
import useFieldRefs from '@hooks/field_refs';
import {t} from '@i18n';
import {getErrorMessage} from '@utils/errors';
import {logError} from '@utils/log';
import {sortCustomProfileAttributes, formatOptionsForSelector, isCustomFieldSamlLinked} from '@utils/user';
@ -40,32 +39,32 @@ type Props = {
const includesSsoService = (sso: string) => ['gitlab', 'google', 'office365'].includes(sso);
const isSAMLOrLDAP = (protocol: string) => ['ldap', 'saml'].includes(protocol);
const FIELDS: { [id: string]: MessageDescriptor } = {
const FIELDS: {[id: string]: MessageDescriptor} = defineMessages({
firstName: {
id: t('user.settings.general.firstName'),
id: 'user.settings.general.firstName',
defaultMessage: 'First Name',
},
lastName: {
id: t('user.settings.general.lastName'),
id: 'user.settings.general.lastName',
defaultMessage: 'Last Name',
},
username: {
id: t('user.settings.general.username'),
id: 'user.settings.general.username',
defaultMessage: 'Username',
},
nickname: {
id: t('user.settings.general.nickname'),
id: 'user.settings.general.nickname',
defaultMessage: 'Nickname',
},
position: {
id: t('user.settings.general.position'),
id: 'user.settings.general.position',
defaultMessage: 'Position',
},
email: {
id: t('user.settings.general.email'),
id: 'user.settings.general.email',
defaultMessage: 'Email',
},
};
});
const styles = StyleSheet.create({
footer: {

View file

@ -12,12 +12,12 @@ import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {usePreventDoubleTap} from '@hooks/utils';
import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
import PanelItem from '@screens/edit_profile/components/panel_item';
import {bottomSheet} from '@screens/navigation';
import PickerUtil from '@utils/file/file_picker';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -73,7 +73,7 @@ const ProfileImagePicker = ({
const styles = getStyleSheet(theme);
const isTablet = useIsTablet();
const showFileAttachmentOptions = useCallback(preventDoubleTap(() => {
const showFileAttachmentOptions = usePreventDoubleTap(useCallback(() => {
const renderContent = () => {
return (
<>
@ -116,7 +116,7 @@ const ProfileImagePicker = ({
title: 'Change profile photo',
theme,
});
}), [canRemovePicture, onRemoveProfileImage, pictureUtils, theme]);
}, [canRemovePicture, isTablet, onRemoveProfileImage, pictureUtils, styles.title, theme]));
return (
<TouchableOpacity

View file

@ -17,10 +17,10 @@ import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import {usePreventDoubleTap} from '@hooks/utils';
import SecurityManager from '@managers/security_manager';
import {dismissModal, popTopScreen, setButtons} from '@screens/navigation';
import {logError} from '@utils/log';
import {preventDoubleTap} from '@utils/tap';
import {isCustomFieldSamlLinked} from '@utils/user';
import ProfileForm, {CUSTOM_ATTRS_PREFIX} from './components/form';
@ -152,7 +152,22 @@ const EditProfile = ({
loadCustomAttributes();
}, [currentUser, serverUrl, enableCustomAttributes, customAttributesSet]);
const submitUser = useCallback(preventDoubleTap(async () => {
const resetScreenForProfileError = useCallback((resetError: unknown) => {
setUsernameError(resetError);
Keyboard.dismiss();
setUpdating(false);
enableSaveButton(true);
}, [enableSaveButton]);
const resetScreen = useCallback((resetError: unknown) => {
setError(resetError);
Keyboard.dismiss();
setUpdating(false);
enableSaveButton(true);
scrollViewRef.current?.scrollToPosition(0, 0, true);
}, [enableSaveButton]);
const submitUser = usePreventDoubleTap(useCallback(async () => {
if (!currentUser) {
return;
}
@ -243,7 +258,22 @@ const EditProfile = ({
} catch (e) {
resetScreen(e);
}
}), [userInfo, enableSaveButton, currentUser, lockedFirstName, lockedLastName, lockedNickname, lockedPosition, customAttributesSet, enableCustomAttributes, customFields, serverUrl]);
}, [
currentUser,
enableSaveButton,
userInfo,
lockedFirstName,
lockedLastName,
lockedNickname,
lockedPosition,
enableCustomAttributes,
close,
serverUrl,
resetScreen,
resetScreenForProfileError,
customFields,
customAttributesSet,
]));
useAndroidHardwareBackHandler(componentId, close);
useNavButtonPressed(UPDATE_BUTTON_ID, componentId, submitUser, [userInfo]);
@ -298,21 +328,6 @@ const EditProfile = ({
enableSaveButton(didChange);
}, [userInfo, currentUser, enableSaveButton]);
const resetScreenForProfileError = useCallback((resetError: unknown) => {
setUsernameError(resetError);
Keyboard.dismiss();
setUpdating(false);
enableSaveButton(true);
}, [enableSaveButton]);
const resetScreen = useCallback((resetError: unknown) => {
setError(resetError);
Keyboard.dismiss();
setUpdating(false);
enableSaveButton(true);
scrollViewRef.current?.scrollToPosition(0, 0, true);
}, [enableSaveButton]);
const content = currentUser ? (
<KeyboardAwareScrollView
bounces={false}

View file

@ -5,7 +5,7 @@ import React, {useCallback} from 'react';
import {TouchableOpacity} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {preventDoubleTap} from '@utils/tap';
import {usePreventDoubleTap} from '@hooks/utils';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
@ -37,7 +37,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const EmojiCategoryBarIcon = ({currentIndex, icon, index, scrollToIndex, theme}: Props) => {
const style = getStyleSheet(theme);
const onPress = useCallback(preventDoubleTap(() => scrollToIndex(index)), []);
const onPress = usePreventDoubleTap(useCallback(() => scrollToIndex(index), [index, scrollToIndex]));
return (
<TouchableOpacity

View file

@ -2,14 +2,13 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {Platform, SectionList, type SectionListRenderItemInfo, StyleSheet} from 'react-native';
import Animated, {FadeInDown, FadeOutUp} from 'react-native-reanimated';
import {switchToChannelById} from '@actions/remote/channel';
import ChannelItem from '@components/channel_item';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import FindChannelsHeader from './header';
@ -23,12 +22,12 @@ type Props = {
testID?: string;
}
const sectionNames = {
const sectionNames = defineMessages({
recent: {
id: t('mobile.channel_list.recent'),
id: 'mobile.channel_list.recent',
defaultMessage: 'Recent',
},
};
});
const style = StyleSheet.create({
flex: {flex: 1},
@ -59,7 +58,7 @@ const UnfilteredList = ({close, keyboardOverlap, recentChannels, showTeamName, t
const renderSectionHeader = useCallback(({section}: SectionListRenderItemInfo<ChannelModel>) => (
<FindChannelsHeader sectionName={intl.formatMessage({id: section.id, defaultMessage: section.defaultMessage})}/>
), [intl.locale]);
), [intl]);
const renderSectionItem = useCallback(({item}: SectionListRenderItemInfo<ChannelModel>) => {
return (
@ -72,7 +71,7 @@ const UnfilteredList = ({close, keyboardOverlap, recentChannels, showTeamName, t
testID={`${testID}.channel_item`}
/>
);
}, [onPress, showTeamName]);
}, [onPress, showTeamName, testID]);
useEffect(() => {
setSections(buildSections(recentChannels));

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {Keyboard, Platform, Text, View} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import {Navigation} from 'react-native-navigation';
@ -91,6 +91,13 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const messages = defineMessages({
reset: {
id: 'password_send.reset',
defaultMessage: 'Reset Your Password',
},
});
const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
const [email, setEmail] = useState<string>('');
const [error, setError] = useState<string>('');
@ -191,8 +198,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
testID={'password_send.link.prepare'}
>
<FormattedText
defaultMessage='Reset Your Password'
id='password_send.reset'
{...messages.reset}
testID='password_send.reset'
style={styles.header}
/>
@ -225,7 +231,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
disabled={!email}
onPress={submitResetPassword}
size='lg'
text={formatMessage({id: 'password_send.reset', defaultMessage: 'Reset my password'})}
text={formatMessage(messages.reset)}
theme={theme}
/>
</View>

View file

@ -78,7 +78,7 @@ describe('GlobalThreads', () => {
expect.any(String),
expect.arrayContaining([
expect.objectContaining({id: 'unreads', name: expect.objectContaining({defaultMessage: 'Unreads'})}),
expect.objectContaining({id: 'all', name: expect.objectContaining({defaultMessage: 'All your threads'})}),
expect.objectContaining({id: 'all', name: expect.objectContaining({defaultMessage: 'All Your Threads'})}),
]),
expect.any(Function),
expect.any(String),

View file

@ -57,7 +57,7 @@ const GlobalThreads = ({componentId, globalThreadsTab, hasUnreads, teamId}: Prop
{
name: defineMessage({
id: 'global_threads.allThreads',
defaultMessage: 'All your threads',
defaultMessage: 'All Your Threads',
}),
id: 'all',
requiresUserAttention: false,

View file

@ -15,9 +15,9 @@ import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {usePreventDoubleTap} from '@hooks/utils';
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
import {getMarkdownTextStyles} from '@utils/markdown';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
@ -142,9 +142,9 @@ const Thread = ({author, channel, location, post, teammateNameDisplay, testID, t
setIsChannelNamePressed((prevState) => !prevState);
}, []);
const showThread = useCallback(preventDoubleTap(() => {
const showThread = usePreventDoubleTap(useCallback(() => {
fetchAndSwitchToThread(serverUrl, thread.id);
}), [serverUrl, thread.id]);
}, [serverUrl, thread.id]));
const onChannelNamePressed = useCallback(() => {
if (channel?.id) {
@ -161,7 +161,7 @@ const Thread = ({author, channel, location, post, teammateNameDisplay, testID, t
} else {
showModalOverCurrentContext(Screens.THREAD_OPTIONS, passProps);
}
}, [isTablet, theme, thread]);
}, [intl, isTablet, theme, thread]);
if (!post || !channel) {
return null;

View file

@ -11,8 +11,8 @@ import {Events, Screens} from '@constants';
import {SET_CUSTOM_STATUS_FAILURE} from '@constants/custom_status';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {usePreventDoubleTap} from '@hooks/utils';
import {showModal} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {getUserCustomStatus, isCustomStatusExpired as checkCustomStatusIsExpired} from '@utils/user';
@ -62,7 +62,7 @@ const CustomStatus = ({isTablet, currentUser}: CustomStatusProps) => {
return () => listener.remove();
}, []);
const clearCustomStatus = useCallback(preventDoubleTap(async () => {
const clearCustomStatus = usePreventDoubleTap(useCallback(async () => {
setShowRetryMessage(false);
const {error} = await unsetCustomStatus(serverUrl);
@ -72,16 +72,16 @@ const CustomStatus = ({isTablet, currentUser}: CustomStatusProps) => {
}
updateLocalCustomStatus(serverUrl, currentUser, undefined);
}), []);
}, [currentUser, serverUrl]));
const goToCustomStatusScreen = useCallback(preventDoubleTap(() => {
const goToCustomStatusScreen = usePreventDoubleTap(useCallback(() => {
if (isTablet) {
DeviceEventEmitter.emit(Events.ACCOUNT_SELECT_TABLET_VIEW, Screens.CUSTOM_STATUS);
} else {
showModal(Screens.CUSTOM_STATUS, intl.formatMessage({id: 'mobile.routes.custom_status', defaultMessage: 'Set a custom status'}));
}
setShowRetryMessage(false);
}), [isTablet]);
}, [intl, isTablet]));
return (
<TouchableOpacity

View file

@ -25,7 +25,7 @@ const LogOut = () => {
return (
<OptionItem
action={onLogout}
description={intl.formatMessage({id: 'account.logout_from', defaultMessage: 'Log out from'}, {serverName: serverDisplayName})}
description={intl.formatMessage({id: 'account.logout_from', defaultMessage: 'Log out of {serverName}'}, {serverName: serverDisplayName})}
destructive={true}
icon='exit-to-app'
label={intl.formatMessage({id: 'account.logout', defaultMessage: 'Log out'})}

View file

@ -14,10 +14,10 @@ import General from '@constants/general';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {usePreventDoubleTap} from '@hooks/utils';
import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
import {bottomSheet, dismissBottomSheet, dismissModal} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {confirmOutOfOfficeDisabled} from '@utils/user';
@ -62,7 +62,29 @@ const UserStatus = ({currentUser}: Props) => {
const isTablet = useIsTablet();
const handleSetStatus = useCallback(preventDoubleTap(() => {
const updateStatus = useCallback((status: string) => {
const userStatus = {
user_id: currentUser.id,
status,
manual: true,
last_activity_at: Date.now(),
};
setStatus(serverUrl, userStatus);
}, [currentUser.id, serverUrl]);
const setUserStatus = useCallback((status: string) => {
if (currentUser.status === OUT_OF_OFFICE) {
dismissModal();
return confirmOutOfOfficeDisabled(intl, status, updateStatus);
}
updateStatus(status);
dismissBottomSheet();
return null;
}, [currentUser.status, intl, updateStatus]);
const handleSetStatus = usePreventDoubleTap(useCallback(() => {
const renderContent = () => {
return (
<>
@ -131,29 +153,13 @@ const UserStatus = ({currentUser}: Props) => {
title: intl.formatMessage({id: 'user_status.title', defaultMessage: 'Status'}),
theme,
});
}), [theme]);
const updateStatus = useCallback((status: string) => {
const userStatus = {
user_id: currentUser.id,
status,
manual: true,
last_activity_at: Date.now(),
};
setStatus(serverUrl, userStatus);
}, []);
const setUserStatus = useCallback((status: string) => {
if (currentUser.status === OUT_OF_OFFICE) {
dismissModal();
return confirmOutOfOfficeDisabled(intl, status, updateStatus);
}
updateStatus(status);
dismissBottomSheet();
return null;
}, []);
}, [
intl,
isTablet,
setUserStatus,
styles,
theme,
]));
return (
<TouchableOpacity

View file

@ -82,7 +82,7 @@ const UnreadCategories = ({onChannelSwitch, onlyUnreads, unreadChannels, unreadT
<Text
style={styles.heading}
>
{intl.formatMessage({id: 'mobile.channel_list.unreads', defaultMessage: 'UNREADS'})}
{intl.formatMessage({id: 'mobile.channel_list.unreads', defaultMessage: 'Unreads'})}
</Text>
}
<FlatList

View file

@ -8,8 +8,8 @@ import {TouchableHighlight} from 'react-native';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import {useTheme} from '@context/theme';
import {usePreventDoubleTap} from '@hooks/utils';
import {findChannels} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -42,12 +42,12 @@ const SearchField = () => {
const intl = useIntl();
const styles = getStyleSheet(theme);
const onPress = useCallback(preventDoubleTap(() => {
const onPress = usePreventDoubleTap(useCallback(() => {
findChannels(
intl.formatMessage({id: 'find_channels.title', defaultMessage: 'Find Channels'}),
theme,
);
}), [intl.locale, theme]);
}, [intl, theme]));
return (
<TouchableHighlight

View file

@ -52,22 +52,22 @@ const getModifiersSectionsData = (intl: IntlShape, teamId: string): ModifierItem
sectionsData.push({
term: 'From:',
testID: 'search.modifier.from',
description: formatMessage({id: 'mobile.search.modifier.from', defaultMessage: ' a specific user'}),
description: formatMessage({id: 'mobile.search.modifier.from', defaultMessage: 'a specific user'}),
}, {
term: 'In:',
testID: 'search.modifier.in',
description: formatMessage({id: 'mobile.search.modifier.in', defaultMessage: ' a specific channel'}),
description: formatMessage({id: 'mobile.search.modifier.in', defaultMessage: 'a specific channel'}),
});
}
sectionsData.push({
term: '-',
testID: 'search.modifier.exclude',
description: formatMessage({id: 'mobile.search.modifier.exclude', defaultMessage: ' exclude search terms'}),
description: formatMessage({id: 'mobile.search.modifier.exclude', defaultMessage: 'exclude search terms'}),
}, {
term: '""',
testID: 'search.modifier.phrases',
description: formatMessage({id: 'mobile.search.modifier.phrases', defaultMessage: ' messages with phrases'}),
description: formatMessage({id: 'mobile.search.modifier.phrases', defaultMessage: 'messages with phrases'}),
cursorPosition: -1,
});

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useRef, useState} from 'react';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {DeviceEventEmitter, Platform, StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import {GestureDetector, Gesture, GestureHandlerRootView} from 'react-native-gesture-handler';
import {Navigation} from 'react-native-navigation';
@ -12,9 +12,9 @@ import {openNotification} from '@actions/remote/notifications';
import {Navigation as NavigationTypes} from '@constants';
import DatabaseManager from '@database/manager';
import {useIsTablet} from '@hooks/device';
import {usePreventDoubleTap} from '@hooks/utils';
import SecurityManager from '@managers/security_manager';
import {dismissOverlay} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity} from '@utils/theme';
import {secureGetFromRecord} from '@utils/types';
@ -95,15 +95,15 @@ const InAppNotification = ({componentId, serverName, serverUrl, notification}: I
}
};
const dismiss = () => {
const dismiss = useCallback(() => {
cancelDismissTimer();
dismissOverlay(componentId);
};
}, [componentId]);
const notificationTapped = preventDoubleTap(() => {
const notificationTapped = usePreventDoubleTap(useCallback(() => {
tapped.current = true;
dismiss();
});
}, [dismiss]));
useEffect(() => {
initial.value = 0;

Some files were not shown because too many files have changed in this diff Show more