diff --git a/app/screens/convert_gm_to_channel/convert_gm_to_channel.tsx b/app/screens/convert_gm_to_channel/convert_gm_to_channel.tsx index 8c3cb1d34..556fd046f 100644 --- a/app/screens/convert_gm_to_channel/convert_gm_to_channel.tsx +++ b/app/screens/convert_gm_to_channel/convert_gm_to_channel.tsx @@ -10,6 +10,8 @@ import {useServerUrl} from '@context/server'; import ConvertGMToChannelForm from './convert_gm_to_channel_form'; import {Loader} from './loader'; +import type UserProfile from '../user_profile/user_profile'; + type Props = { channelId: string; currentUserId?: string; @@ -30,21 +32,49 @@ const ConvertGMToChannel = ({ const serverUrl = useServerUrl(); const mounted = useRef(false); + const loadingAnimationTimeoutRef = useRef(); + useEffect(() => { - setTimeout(() => setLoadingAnimationTimeout(true), loadingIndicatorTimeout); + loadingAnimationTimeoutRef.current = setTimeout(() => setLoadingAnimationTimeout(true), loadingIndicatorTimeout); async function work() { const {teams} = await getGroupMessageMembersCommonTeams(serverUrl, channelId); - if (!teams) { + if (!teams || !mounted.current) { return; } - setCommonTeams(teams); setCommonTeamsFetched(true); } work(); + + return () => { + if (loadingAnimationTimeoutRef.current) { + clearTimeout(loadingAnimationTimeoutRef.current); + } + }; }, []); + const matchUserProfiles = (users: UserProfile[], members: ChannelMembership[]) => { + // Gotta make sure we use profiles that are in members. + // See comment in fetchChannelMemberships for more details. + + const usersById: {[id: string]: UserProfile} = {}; + users.forEach((profile) => { + if (profile.id !== currentUserId) { + usersById[profile.id] = profile; + } + }); + + const filteredUsers: UserProfile[] = []; + members.forEach((member) => { + if (usersById[member.user_id]) { + filteredUsers.push(usersById[member.user_id]); + } + }); + + return filteredUsers; + }; + useEffect(() => { mounted.current = true; @@ -55,26 +85,7 @@ const ConvertGMToChannel = ({ } if (users.length) { - // Gotta make sure we use profiles that are in members. - // See comment in fetchChannelMemberships for more details. - - const usersById: {[id: string]: UserProfile} = {}; - // eslint-disable-next-line max-nested-callbacks - users.forEach((profile) => { - if (profile.id !== currentUserId) { - usersById[profile.id] = profile; - } - }); - - // eslint-disable-next-line max-nested-callbacks - const filteredUsers: UserProfile[] = []; - // eslint-disable-next-line max-nested-callbacks - members.forEach((member) => { - if (usersById[member.user_id]) { - filteredUsers.push(usersById[member.user_id]); - } - }); - setProfiles(filteredUsers); + setProfiles(matchUserProfiles(users, members)); } setChannelMembersFetched(true); diff --git a/app/screens/convert_gm_to_channel/convert_gm_to_channel_form/convert_gm_to_channel_form.tsx b/app/screens/convert_gm_to_channel/convert_gm_to_channel_form/convert_gm_to_channel_form.tsx index 19e3690a5..2bfe01f86 100644 --- a/app/screens/convert_gm_to_channel/convert_gm_to_channel_form/convert_gm_to_channel_form.tsx +++ b/app/screens/convert_gm_to_channel/convert_gm_to_channel_form/convert_gm_to_channel_form.tsx @@ -3,15 +3,16 @@ import React, {useCallback, useEffect, useState} from 'react'; import {useIntl} from 'react-intl'; -import {View} from 'react-native'; +import {Text, View} from 'react-native'; import {convertGroupMessageToPrivateChannel, switchToChannelById} from '@actions/remote/channel'; +import {isErrorWithMessage} from '@app/utils/errors'; +import {preventDoubleTap} from '@app/utils/tap'; +import Button from '@components/button'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; -import {logError} from '@utils/log'; import {makeStyleSheetFromTheme} from '@utils/theme'; import {displayUsername} from '@utils/user'; -import Button from '@components/button'; import {ChannelNameInput} from '../channel_name_input'; import MessageBox from '../message_box/message_box'; @@ -19,7 +20,7 @@ import {TeamSelector} from '../team_selector'; import {NoCommonTeamForm} from './no_common_teams_form'; -const getStyleFromTheme = makeStyleSheetFromTheme(() => { +const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { return { container: { paddingVertical: 24, @@ -28,6 +29,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme(() => { flexDirection: 'column', gap: 24, }, + errorMessage: { + color: theme.dndIndicator, + }, }; }); @@ -53,6 +57,7 @@ export const ConvertGMToChannelForm = ({ const [selectedTeam, setSelectedTeam] = useState(); const [newChannelName, setNewChannelName] = useState(''); + const [errorMessage, setErrorMessage] = useState('Something went wrong. Failed to convert Group Message to Private Channel'); const submitButtonEnabled = selectedTeam && newChannelName.trim(); @@ -66,26 +71,32 @@ export const ConvertGMToChannelForm = ({ if (commonTeams.length > 0) { setSelectedTeam(commonTeams[0]); } - }, [commonTeams]); + }, []); - const handleOnPress = useCallback(async () => { + const handleOnPress = useCallback(preventDoubleTap(async () => { if (!submitButtonEnabled) { return; } const {updatedChannel, error} = await convertGroupMessageToPrivateChannel(serverUrl, channelId, selectedTeam.id, newChannelName); - if (error) { - logError(error); + if (isErrorWithMessage(error)) { + setErrorMessage(error.message); + } else { + setErrorMessage(formatMessage({id: 'channel_info.convert_gm_to_channel.conversion_error', defaultMessage: 'Something went wrong. Failed to convert Group Message to Private Channel.'})); + } + return; } if (!updatedChannel) { + setErrorMessage(formatMessage({id: 'channel_info.convert_gm_to_channel.conversion_error', defaultMessage: 'Something went wrong. Failed to convert Group Message to Private Channel.'})); return; } - await switchToChannelById(serverUrl, updatedChannel.id, selectedTeam.id); - }, [selectedTeam, newChannelName, submitButtonEnabled]); + setErrorMessage(''); + switchToChannelById(serverUrl, updatedChannel.id, selectedTeam.id); + }), [selectedTeam, newChannelName, submitButtonEnabled]); const messageBoxHeader = intl.formatMessage({ id: 'channel_info.convert_gm_to_channel.warning.header', @@ -130,6 +141,12 @@ export const ConvertGMToChannelForm = ({ buttonType={submitButtonEnabled ? 'destructive' : 'disabled'} size='lg' /> + { + errorMessage && + + {errorMessage} + + } ); }; diff --git a/app/screens/convert_gm_to_channel/convert_gm_to_channel_form/no_common_teams_form.tsx b/app/screens/convert_gm_to_channel/convert_gm_to_channel_form/no_common_teams_form.tsx index e2ffd8ba4..feabdcd21 100644 --- a/app/screens/convert_gm_to_channel/convert_gm_to_channel_form/no_common_teams_form.tsx +++ b/app/screens/convert_gm_to_channel/convert_gm_to_channel_form/no_common_teams_form.tsx @@ -5,11 +5,12 @@ import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; import {View, type ViewStyle} from 'react-native'; +import {preventDoubleTap} from '@app/utils/tap'; import Button from '@components/button'; import {useTheme} from '@context/theme'; import {popTopScreen} from '@screens/navigation'; -import MessageBox from '../message_box'; +import MessageBox from '../message_box/message_box'; type Props = { containerStyles: ViewStyle; @@ -31,9 +32,9 @@ export const NoCommonTeamForm = ({ defaultMessage: 'Group Message cannot be converted to a channel because members are not a part of the same team. Add all members to a single team to convert this group message to a channel.', }); - const handleOnPress = useCallback(() => { + const handleOnPress = useCallback(preventDoubleTap(() => { popTopScreen(); - }, []); + }), []); const buttonText = formatMessage({ id: 'generic.back', diff --git a/app/screens/convert_gm_to_channel/message_box/message_box.tsx b/app/screens/convert_gm_to_channel/message_box/message_box.tsx index f759a076a..7fc557244 100644 --- a/app/screens/convert_gm_to_channel/message_box/message_box.tsx +++ b/app/screens/convert_gm_to_channel/message_box/message_box.tsx @@ -85,8 +85,6 @@ const getDangerStylesFromTheme = makeStyleSheetFromTheme((theme: Theme) => { }); const getStyleFromTheme = (theme: Theme, kind: MessageBoxTypes | undefined) => { - const baseStyles = getBaseStyles(theme); - let kindStyles; switch (kind) { case 'danger': { @@ -100,7 +98,6 @@ const getStyleFromTheme = (theme: Theme, kind: MessageBoxTypes | undefined) => { } return { - ...baseStyles, ...kindStyles, }; }; @@ -111,25 +108,25 @@ const MessageBox = ({ type, }: Props) => { const theme = useTheme(); - const styles = getBaseStyles(theme); - const foo = getStyleFromTheme(theme, type); + const baseStyle = getBaseStyles(theme); + const kindStyle = getStyleFromTheme(theme, type); return ( - - + + - + - + {header} - + {body} diff --git a/app/screens/convert_gm_to_channel/team_selector.tsx b/app/screens/convert_gm_to_channel/team_selector.tsx index b539eccc1..7ad75dd27 100644 --- a/app/screens/convert_gm_to_channel/team_selector.tsx +++ b/app/screens/convert_gm_to_channel/team_selector.tsx @@ -39,7 +39,7 @@ export const TeamSelector = ({commonTeams, onSelectTeam, selectedTeamId}: Props) const placeholder = formatMessage({id: 'channel_into.convert_gm_to_channel.team_selector.placeholder', defaultMessage: 'Select a Team'}); useEffect(() => { - if (selectedTeamId) { + if (selectedTeamId && !selectedTeam) { const team = commonTeams.find((t) => t.id === selectedTeamId); setSelectedTeam(team); } diff --git a/app/screens/convert_gm_to_channel/team_selector_list/index.tsx b/app/screens/convert_gm_to_channel/team_selector_list/index.tsx index 9c023f890..948b7f8b9 100644 --- a/app/screens/convert_gm_to_channel/team_selector_list/index.tsx +++ b/app/screens/convert_gm_to_channel/team_selector_list/index.tsx @@ -1,6 +1,69 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import TeamSelectorList from './team_selector_list'; +import React, {useCallback, useMemo, useState} from 'react'; +import {StyleSheet, View} from 'react-native'; + +import {preventDoubleTap} from '@app/utils/tap'; +import SearchBar from '@components/search'; +import TeamList from '@components/team_list'; +import {useTheme} from '@context/theme'; +import {popTopScreen} from '@screens/navigation'; +import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme'; + +const styles = StyleSheet.create({ + container: { + padding: 12, + }, + listContainer: { + marginTop: 12, + }, +}); + +type Props = { + teams: Team[]; + selectTeam: (teamId: string) => void; +} + +const TeamSelectorList = ({teams, selectTeam}: Props) => { + const theme = useTheme(); + + const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]); + + const [filteredTeams, setFilteredTeam] = useState(teams); + + const handleOnChangeSearchText = useCallback((searchTerm: string) => { + if (searchTerm === '') { + setFilteredTeam(teams); + } else { + setFilteredTeam(teams.filter((team) => team.display_name.includes(searchTerm) || team.name.includes(searchTerm))); + } + }, [teams]); + + const handleOnPress = useCallback(preventDoubleTap((teamId: string) => { + selectTeam(teamId); + popTopScreen(); + }), []); + + return ( + + + + + + + ); +}; export default TeamSelectorList; diff --git a/app/screens/convert_gm_to_channel/team_selector_list/team_selector_list.tsx b/app/screens/convert_gm_to_channel/team_selector_list/team_selector_list.tsx deleted file mode 100644 index 6a575ac0f..000000000 --- a/app/screens/convert_gm_to_channel/team_selector_list/team_selector_list.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useCallback, useMemo, useState} from 'react'; -import {View} from 'react-native'; - -import SearchBar from '@components/search'; -import TeamList from '@components/team_list'; -import {useTheme} from '@context/theme'; -import {popTopScreen} from '@screens/navigation'; -import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme'; - -const style = StyleSheet.create({ - container: { - padding: 12, - }, - listContainer: { - marginTop: 12, - }, -}); - -type Props = { - teams: Team[]; - selectTeam: (teamId: string) => void; -} - -const TeamSelectorList = ({teams, selectTeam}: Props) => { - const theme = useTheme(); - const styles = getStyleSheet(theme); - - const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]); - - const [filteredTeams, setFilteredTeam] = useState(teams); - - const handleOnChangeSearchText = useCallback((searchTerm: string) => { - if (searchTerm === '') { - setFilteredTeam(teams); - } else { - setFilteredTeam(teams.filter((team) => team.display_name.includes(searchTerm) || team.name.includes(searchTerm))); - } - }, [teams]); - - const handleOnPress = useCallback((teamId: string) => { - selectTeam(teamId); - popTopScreen(); - }, []); - - return ( - - - - - - - ); -}; - -export default TeamSelectorList; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 1b23dc3ea..c484e471f 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -1099,6 +1099,7 @@ "channel_into.convert_gm_to_channel.team_selector.label": "Team", "channel_into.convert_gm_to_channel.team_selector.placeholder": "Select a Team", "channel_info.convert_gm_to_channel.button_text": "Convert to Private Channel", + "channel_info.convert_gm_to_channel.conversion_error": "Something went wrong. Failed to convert Group Message to Private Channel.", "channel_info.convert_gm_to_channel.team_selector_list.title": "Select Team", "generic.back": "Back" }