diff --git a/app/actions/remote/scheduled_post.test.ts b/app/actions/remote/scheduled_post.test.ts index ea41f9255a..e165291a53 100644 --- a/app/actions/remote/scheduled_post.test.ts +++ b/app/actions/remote/scheduled_post.test.ts @@ -210,7 +210,7 @@ describe('updateScheduledPost', () => { const spyHandleScheduledPosts = jest.spyOn(operator, 'handleScheduledPosts'); await operator.handleUsers({users: [user1], prepareRecordsOnly: false}); const mockResponse = {...scheduledPost, update_at: Date.now()}; - mockClient.updateScheduledPost.mockImplementationOnce(() => mockResponse); + mockClient.updateScheduledPost.mockImplementationOnce(() => Promise.resolve(mockResponse)); const result = await updateScheduledPost(serverUrl, scheduledPost, undefined, true); expect(result.error).toBeUndefined(); expect(result.scheduledPost).toEqual(mockResponse); diff --git a/app/constants/snack_bar.ts b/app/constants/snack_bar.ts index 77e1899870..a051375b96 100644 --- a/app/constants/snack_bar.ts +++ b/app/constants/snack_bar.ts @@ -20,6 +20,8 @@ export const SNACK_BAR_TYPE = keyMirror({ UNMUTE_CHANNEL: null, UNFOLLOW_THREAD: null, SCHEDULED_POST_CREATION_ERROR: null, + RESCHEDULED_POST: null, + DELETE_SCHEDULED_POST_ERROR: null, }); export const MESSAGE_TYPE = { diff --git a/app/hooks/handle_send_message.test.tsx b/app/hooks/handle_send_message.test.tsx index 2692997abb..8f4f9b40ba 100644 --- a/app/hooks/handle_send_message.test.tsx +++ b/app/hooks/handle_send_message.test.tsx @@ -622,4 +622,31 @@ describe('useHandleSendMessage', () => { expect(createScheduledPost).toHaveBeenCalled(); }); }); + + describe('handle error while failing creating post from scheduled post and draft', () => { + it('should handle failed post creation', async () => { + jest.mocked(createPost).mockResolvedValueOnce({ + error: new Error('Failed to create post'), + }); + + jest.mock('@utils/snack_bar', () => ({ + showSnackBar: jest.fn(), + })); + + const props = { + ...defaultProps, + value: 'test message', + }; + + const {result} = renderHook(() => useHandleSendMessage(props), {wrapper}); + + await act(async () => { + const response = await result.current.handleSendMessage(); + expect(response?.error).toBeDefined(); + }); + + expect(defaultProps.clearDraft).toHaveBeenCalled(); + expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.POST_LIST_SCROLL_TO_BOTTOM, Screens.CHANNEL); + }); + }); }); diff --git a/app/screens/draft_scheduled_post_options/send_draft.tsx b/app/screens/draft_scheduled_post_options/send_draft.tsx index 6867c81853..99308878ee 100644 --- a/app/screens/draft_scheduled_post_options/send_draft.tsx +++ b/app/screens/draft_scheduled_post_options/send_draft.tsx @@ -11,6 +11,7 @@ import FormattedText from '@components/formatted_text'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {General} from '@constants'; import {ICON_SIZE} from '@constants/post_draft'; +import {SNACK_BAR_TYPE} from '@constants/snack_bar'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useHandleSendMessage} from '@hooks/handle_send_message'; @@ -18,6 +19,7 @@ import {usePersistentNotificationProps} from '@hooks/persistent_notification_pro import {DRAFT_TYPE_DRAFT, type DraftType} from '@screens/global_drafts/constants'; import {dismissBottomSheet} from '@screens/navigation'; import {persistentNotificationsConfirmation, sendMessageWithAlert} from '@utils/post'; +import {showSnackBar} from '@utils/snack_bar'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -92,13 +94,22 @@ const SendDraft: React.FC = ({ const intl = useIntl(); const style = getStyleSheet(theme); const serverUrl = useServerUrl(); - const clearDraft = () => { + + const clearDraft = async () => { if (draftType === DRAFT_TYPE_DRAFT) { removeDraft(serverUrl, channelId, rootId); return; } if (postId) { - deleteScheduledPost(serverUrl, postId); + const res = await deleteScheduledPost(serverUrl, postId); + if (res?.error) { + showSnackBar({ + barType: SNACK_BAR_TYPE.DELETE_SCHEDULED_POST_ERROR, + customMessage: (res.error as Error).message, + keepOpen: true, + type: 'error', + }); + } } }; diff --git a/app/screens/reschedule_draft/reschedule_draft.tsx b/app/screens/reschedule_draft/reschedule_draft.tsx index 1edb2c7112..369b2a4b0b 100644 --- a/app/screens/reschedule_draft/reschedule_draft.tsx +++ b/app/screens/reschedule_draft/reschedule_draft.tsx @@ -7,15 +7,16 @@ import {Keyboard, SafeAreaView, StyleSheet, View} from 'react-native'; import {updateScheduledPost} from '@actions/remote/scheduled_post'; import Loading from '@components/loading'; +import {SNACK_BAR_TYPE} from '@constants/snack_bar'; import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import DatabaseManager from '@database/manager'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useNavButtonPressed from '@hooks/navigation_button_pressed'; import DateTimeSelector from '@screens/custom_status_clear_after/components/date_time_selector'; -import PostError from '@screens/edit_post/post_error'; import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation'; import {logDebug} from '@utils/log'; +import {showSnackBar} from '@utils/snack_bar'; import {changeOpacity} from '@utils/theme'; import {getTimezone} from '@utils/user'; @@ -63,8 +64,6 @@ const RescheduledDraft: React.FC = ({ const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const mainView = useRef(null); const [isUpdating, setIsUpdating] = useState(false); - const [errorLine, setErrorLine] = useState(); - const [errorExtra, setErrorExtra] = useState(); const selectedTime = useRef(null); const userTimezone = getTimezone(currentUserTimezone); @@ -93,8 +92,13 @@ const RescheduledDraft: React.FC = ({ const handleUIUpdates = useCallback((res: {error?: unknown}) => { if (res.error) { setIsUpdating(false); - const errorMessage = intl.formatMessage({id: 'mobile.scheduled_post.update.error', defaultMessage: 'There was a problem editing this message. Please try again.'}); - setErrorLine(errorMessage); + const errorMessage = intl.formatMessage({id: 'mobile.scheduled_post.update.error', defaultMessage: 'There was a problem updating this post message. Please try again.'}); + showSnackBar({ + barType: SNACK_BAR_TYPE.RESCHEDULED_POST, + customMessage: errorMessage, + keepOpen: true, + type: 'error', + }); } else { setIsUpdating(false); onClose(); @@ -103,14 +107,17 @@ const RescheduledDraft: React.FC = ({ const onSavePostMessage = useCallback(async () => { setIsUpdating(true); - setErrorLine(undefined); - setErrorExtra(undefined); toggleSaveButton(false); if (!selectedTime.current) { logDebug('ScheduledPostOptions', 'No time selected'); setIsUpdating(false); const errorMessage = intl.formatMessage({id: 'mobile.scheduled_post.error', defaultMessage: 'No time selected'}); - setErrorLine(errorMessage); + showSnackBar({ + barType: SNACK_BAR_TYPE.RESCHEDULED_POST, + customMessage: errorMessage, + keepOpen: true, + type: 'error', + }); return; } const draftPayload = await draft.toApi(database); @@ -148,12 +155,6 @@ const RescheduledDraft: React.FC = ({ style={styles.body} ref={mainView} > - {Boolean((errorLine || errorExtra)) && - - } ; }) { const deleteScheduledPostOnConfirm = async () => { - await deleteScheduledPost(serverUrl, scheduledPostId); + const res = await deleteScheduledPost(serverUrl, scheduledPostId); + if (res?.error) { + showSnackBar({ + barType: SNACK_BAR_TYPE.DELETE_SCHEDULED_POST_ERROR, + customMessage: (res.error as Error).message, + keepOpen: true, + type: 'error', + }); + } }; const onDismiss = () => { diff --git a/app/utils/scheduled_post/scheduled_post.test.ts b/app/utils/scheduled_post/scheduled_post.test.ts index 805a2455f1..3a5a1be97d 100644 --- a/app/utils/scheduled_post/scheduled_post.test.ts +++ b/app/utils/scheduled_post/scheduled_post.test.ts @@ -9,6 +9,11 @@ import {deleteScheduledPostConfirmation, getErrorStringFromCode, type ScheduledP import type {IntlShape} from 'react-intl'; import type {SwipeableMethods} from 'react-native-gesture-handler/lib/typescript/components/ReanimatedSwipeable'; +// Mock dependencies before importing the module under test +jest.mock('@utils/snack_bar', () => ({ + showSnackBar: jest.fn(), +})); + jest.mock('@actions/remote/scheduled_post', () => ({ deleteScheduledPost: jest.fn(), })); @@ -17,6 +22,9 @@ jest.mock('react-native', () => ({ Alert: { alert: jest.fn(), }, + Platform: { + select: jest.fn((obj) => obj.ios || obj.default), + }, })); describe('deleteScheduledPostConfirmation', () => { @@ -89,6 +97,27 @@ describe('deleteScheduledPostConfirmation', () => { // Should not throw when swipeable is undefined expect(() => cancelButton.onPress()).not.toThrow(); }); + + it('shows error snackbar when deleteScheduledPost fails', async () => { + const showSnackBar = require('@utils/snack_bar').showSnackBar; + const errorMessage = 'Failed to delete scheduled post'; + (deleteScheduledPost as jest.Mock).mockResolvedValueOnce({ + error: new Error(errorMessage), + }); + + deleteScheduledPostConfirmation(baseProps); + + // Get the confirm button callback + const confirmButton = (Alert.alert as jest.Mock).mock.calls[0][2][1]; + await confirmButton.onPress(); + + expect(showSnackBar).toHaveBeenCalledWith({ + barType: 'DELETE_SCHEDULED_POST_ERROR', + customMessage: errorMessage, + keepOpen: true, + type: 'error', + }); + }); }); describe('getErrorStringFromCode', () => {