diff --git a/app/actions/remote/reactions.ts b/app/actions/remote/reactions.ts index d710fc868..90b601350 100644 --- a/app/actions/remote/reactions.ts +++ b/app/actions/remote/reactions.ts @@ -16,6 +16,35 @@ import {forceLogoutIfNecessary} from './session'; import type {Model} from '@nozbe/watermelondb'; import type PostModel from '@typings/database/models/servers/post'; +export async function getIsReactionAlreadyAddedToPost(serverUrl: string, postId: string, emojiName: string) { + try { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + + const currentUserId = await getCurrentUserId(database); + const emojiAlias = getEmojiFirstAlias(emojiName); + return await queryReaction(database, emojiAlias, postId, currentUserId).fetchCount() > 0; + } catch (error) { + logDebug('error on getIsReactionAlreadyAddedToPost', getFullErrorMessage(error)); + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +} + +export async function toggleReaction(serverUrl: string, postId: string, emojiName: string) { + try { + const isReactionAlreadyAddedToPost = await getIsReactionAlreadyAddedToPost(serverUrl, postId, emojiName); + + if (isReactionAlreadyAddedToPost) { + return removeReaction(serverUrl, postId, emojiName); + } + return addReaction(serverUrl, postId, emojiName); + } catch (error) { + logDebug('error on toggleReaction', getFullErrorMessage(error)); + forceLogoutIfNecessary(serverUrl, error); + return {error}; + } +} + export async function addReaction(serverUrl: string, postId: string, emojiName: string) { try { const client = NetworkManager.getClient(serverUrl); diff --git a/app/components/post_list/post/body/reactions/reactions.tsx b/app/components/post_list/post/body/reactions/reactions.tsx index 9bda75844..2ca18c922 100644 --- a/app/components/post_list/post/body/reactions/reactions.tsx +++ b/app/components/post_list/post/body/reactions/reactions.tsx @@ -5,7 +5,7 @@ import React, {useCallback, useRef, useState} from 'react'; import {useIntl} from 'react-intl'; import {Keyboard, TouchableOpacity} from 'react-native'; -import {addReaction, removeReaction} from '@actions/remote/reactions'; +import {addReaction, removeReaction, toggleReaction} from '@actions/remote/reactions'; import CompassIcon from '@components/compass_icon'; import {Screens} from '@constants'; import {MAX_ALLOWED_REACTIONS} from '@constants/emoji'; @@ -100,8 +100,8 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, return {reactionsByName, highlightedReactions}; }, [sortedReactions, reactions]); - const handleAddReactionToPost = (emoji: string) => { - addReaction(serverUrl, postId, emoji); + const handleToggleReactionToPost = (emoji: string) => { + toggleReaction(serverUrl, postId, emoji); }; const handleAddReaction = useCallback(preventDoubleTap(() => { @@ -110,7 +110,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, screen: Screens.EMOJI_PICKER, theme, title: formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}), - props: {onEmojiPress: handleAddReactionToPost}, + props: {onEmojiPress: handleToggleReactionToPost}, }); }), [formatMessage, theme]); diff --git a/app/constants/server_errors.ts b/app/constants/server_errors.ts index 39810710d..5ca22c999 100644 --- a/app/constants/server_errors.ts +++ b/app/constants/server_errors.ts @@ -7,4 +7,5 @@ export default { PLUGIN_DISMISSED_POST_ERROR: 'plugin.message_will_be_posted.dismiss_post', SEND_EMAIL_WITH_DEFAULTS_ERROR: 'api.team.invite_members.unable_to_send_email_with_defaults.app_error', TEAM_MEMBERSHIP_DENIAL_ERROR_ID: 'api.team.add_members.user_denied', + DUPLICATE_CHANNEL_NAME: 'store.sql_channel.save_channel.exists.app_error', }; diff --git a/app/products/calls/components/floating_call_container.tsx b/app/products/calls/components/floating_call_container.tsx index 0a88911f8..d5c264efa 100644 --- a/app/products/calls/components/floating_call_container.tsx +++ b/app/products/calls/components/floating_call_container.tsx @@ -22,14 +22,22 @@ const style = StyleSheet.create({ }); type Props = { - channelId: string; - showJoinCallBanner: boolean; - showIncomingCalls: boolean; - isInACall: boolean; + channelId?: string; + showJoinCallBanner?: boolean; + showIncomingCalls?: boolean; + isInACall?: boolean; threadScreen?: boolean; + channelsScreen?: boolean; } -const FloatingCallContainer = ({channelId, showJoinCallBanner, showIncomingCalls, isInACall, threadScreen}: Props) => { +const FloatingCallContainer = ({ + channelId, + showJoinCallBanner, + showIncomingCalls, + isInACall, + threadScreen, + channelsScreen, +}: Props) => { const serverUrl = useServerUrl(); const insets = useSafeAreaInsets(); const isTablet = useIsTablet(); @@ -39,10 +47,13 @@ const FloatingCallContainer = ({channelId, showJoinCallBanner, showIncomingCalls const wrapperTop = { top: insets.top + topBarForTablet + topBarChannel, }; + const wrapperBottom = { + bottom: 8, + }; return ( - - {showJoinCallBanner && + + {showJoinCallBanner && channelId && { )), ) as Observable>; }; + +export const observeCallStateInChannel = (serverUrl: string, database: Database, channelId: Observable) => { + const isCallInCurrentChannel = combineLatest([channelId, observeChannelsWithCalls(serverUrl)]).pipe( + switchMap(([id, calls]) => of$(Boolean(calls[id]))), + distinctUntilChanged(), + ); + const currentCall = observeCurrentCall(); + const ccChannelId = currentCall.pipe( + switchMap((call) => of$(call?.channelId)), + distinctUntilChanged(), + ); + const isInACall = currentCall.pipe( + switchMap((call) => of$(Boolean(call?.connected))), + distinctUntilChanged(), + ); + const dismissed = combineLatest([channelId, observeCallsState(serverUrl)]).pipe( + switchMap(([id, state]) => of$(Boolean(state.calls[id]?.dismissed[state.myUserId]))), + distinctUntilChanged(), + ); + const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe( + switchMap(([id, ccId]) => of$(id === ccId)), + distinctUntilChanged(), + ); + const showJoinCallBanner = combineLatest([isCallInCurrentChannel, dismissed, isInCurrentChannelCall]).pipe( + switchMap(([isCall, dism, inCurrCall]) => of$(Boolean(isCall && !dism && !inCurrCall))), + distinctUntilChanged(), + ); + const showIncomingCalls = observeIncomingCalls().pipe( + switchMap((ics) => of$(ics.incomingCalls.length > 0)), + distinctUntilChanged(), + ); + + return { + showJoinCallBanner, + isInACall, + showIncomingCalls, + }; +}; diff --git a/app/screens/channel/index.tsx b/app/screens/channel/index.tsx index 8c7c20419..4974635e0 100644 --- a/app/screens/channel/index.tsx +++ b/app/screens/channel/index.tsx @@ -3,15 +3,9 @@ import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; import withObservables from '@nozbe/with-observables'; -import {combineLatest, distinctUntilChanged, of as of$, switchMap} from 'rxjs'; +import {of as of$, switchMap} from 'rxjs'; -import {observeIsCallsEnabledInChannel} from '@calls/observers'; -import { - observeCallsState, - observeChannelsWithCalls, - observeCurrentCall, - observeIncomingCalls, -} from '@calls/state'; +import {observeCallStateInChannel, observeIsCallsEnabledInChannel} from '@calls/observers'; import {Preferences} from '@constants'; import {withServerUrl} from '@context/server'; import {observeCurrentChannel} from '@queries/servers/channel'; @@ -29,37 +23,6 @@ type EnhanceProps = WithDatabaseArgs & { const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => { const channelId = observeCurrentChannelId(database); - - const isCallInCurrentChannel = combineLatest([channelId, observeChannelsWithCalls(serverUrl)]).pipe( - switchMap(([id, calls]) => of$(Boolean(calls[id]))), - distinctUntilChanged(), - ); - const currentCall = observeCurrentCall(); - const ccChannelId = currentCall.pipe( - switchMap((call) => of$(call?.channelId)), - distinctUntilChanged(), - ); - const isInACall = currentCall.pipe( - switchMap((call) => of$(Boolean(call?.connected))), - distinctUntilChanged(), - ); - const dismissed = combineLatest([channelId, observeCallsState(serverUrl)]).pipe( - switchMap(([id, state]) => of$(Boolean(state.calls[id]?.dismissed[state.myUserId]))), - distinctUntilChanged(), - ); - const isInCurrentChannelCall = combineLatest([channelId, ccChannelId]).pipe( - switchMap(([id, ccId]) => of$(id === ccId)), - distinctUntilChanged(), - ); - const showJoinCallBanner = combineLatest([isCallInCurrentChannel, dismissed, isInCurrentChannelCall]).pipe( - switchMap(([isCall, dism, inCurrCall]) => of$(Boolean(isCall && !dism && !inCurrCall))), - distinctUntilChanged(), - ); - const showIncomingCalls = observeIncomingCalls().pipe( - switchMap((ics) => of$(ics.incomingCalls.length > 0)), - distinctUntilChanged(), - ); - const dismissedGMasDMNotice = queryPreferencesByCategoryAndName(database, Preferences.CATEGORIES.SYSTEM_NOTICE, Preferences.NOTICES.GM_AS_DM).observe(); const channelType = observeCurrentChannel(database).pipe(switchMap((c) => of$(c?.type))); const currentUserId = observeCurrentUserId(database); @@ -67,9 +30,7 @@ const enhanced = withObservables([], ({database, serverUrl}: EnhanceProps) => { return { channelId, - showJoinCallBanner, - isInACall, - showIncomingCalls, + ...observeCallStateInChannel(serverUrl, database, channelId), isCallsEnabledInChannel: observeIsCallsEnabledInChannel(database, serverUrl, channelId), dismissedGMasDMNotice, channelType, 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 acfaad9ea..a38f0392e 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 @@ -8,9 +8,10 @@ import {Text, View} from 'react-native'; import {convertGroupMessageToPrivateChannel, switchToChannelById} from '@actions/remote/channel'; import Button from '@components/button'; import Loading from '@components/loading'; +import {ServerErrors} from '@constants'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; -import {isErrorWithMessage} from '@utils/errors'; +import {isErrorWithMessage, isServerError} from '@utils/errors'; import {logError} from '@utils/log'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -65,6 +66,7 @@ export const ConvertGMToChannelForm = ({ const [selectedTeam, setSelectedTeam] = useState(commonTeams[0]); const [newChannelName, setNewChannelName] = useState(''); const [errorMessage, setErrorMessage] = useState(''); + const [channelNameErrorMessage, setChannelNameErrorMessage] = useState(''); const [conversionInProgress, setConversionInProgress] = useState(false); const userDisplayNames = useMemo(() => profiles.map((profile) => displayUsername(profile, locale, teammateNameDisplay)), [profiles, teammateNameDisplay, locale]); @@ -79,7 +81,9 @@ export const ConvertGMToChannelForm = ({ const {updatedChannel, error} = await convertGroupMessageToPrivateChannel(serverUrl, channelId, selectedTeam.id, newChannelName); if (error) { - if (isErrorWithMessage(error)) { + if (isServerError(error) && error.server_error_id === ServerErrors.DUPLICATE_CHANNEL_NAME && isErrorWithMessage(error)) { + setChannelNameErrorMessage(error.message); + } else 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.'})); @@ -153,7 +157,10 @@ export const ConvertGMToChannelForm = ({ selectedTeamId={selectedTeam?.id} /> } - +