From 9e77c419b1bef8d6595d64cd6f01414de3970908 Mon Sep 17 00:00:00 2001 From: Avinash Lingaloo Date: Sun, 13 Mar 2022 00:22:24 +0400 Subject: [PATCH] MM-41991 Gekidou Edit Post Screen (#6016) * edit screen - in progress * edit screen - in progress * edit post screen - post input - in progress * edit post screen - post input - in progress * edit post screen - post input - in progress * edit post screen - post input - in progress * edit post screen - post error component - in progress * edit post screen - post error component - in progress * edit post screen -emitEditing - in progress * edit post screen - in progress * edit post screen - in progress * edit post screen - in progress * able to edit post * edit post screen - in progress * edit post screen - in progress * edit post screen - in progress * edit post screen - in progress * updated errorLine * corrections * edit post screen - in progress * edit post screen - in progress * edit post screen - in progress * properly closes modal on tablets * starts with Save button set to false * refactored onTextSelectionChange * added useTheme to ErrorTextComponent * passing canEdit and hasFilesAttached * passing canEdit and hasFilesAttached * fix API call * change canEdit to canDelete * nearly there * displays alert * maxPostSize * autocomplete - fixing layout * autocomplete - fixing layout * autocomplete - work in progress * autocomplete - work in progress * clean up delete * fixing autocomplete * code fix * added server error message * update i18n * removed comment * code fix * fix bug on empty post message * post input top corrections * post draft limit * code corrections as per review * removed theme from useEffect * update edit_post - delete call * refactor PostInputRef to EditPostInputRef * autocomplete position fix and feedback addressed * Navigation title & subtitle fonts / navigation button builder * ux feedback * delay focus of edit input by 20 frames * properly dismiss the PostOptions screen this comes from the fix for the BottomSheet screen * using device info to check for physical keyboard * autocomplete with keyboard closed Co-authored-by: Elias Nahum --- app/actions/remote/post.ts | 33 ++ app/components/autocomplete/autocomplete.tsx | 13 +- app/components/error_text/index.tsx | 5 +- app/constants/screens.ts | 2 + app/screens/edit_post/edit_post.tsx | 308 ++++++++++++++++++ .../edit_post/edit_post_input/index.tsx | 93 ++++++ app/screens/edit_post/index.tsx | 32 ++ app/screens/edit_post/post_error/index.tsx | 57 ++++ .../edit_profile/components/profile_error.tsx | 1 - app/screens/index.tsx | 164 +++++----- app/screens/navigation.ts | 24 +- app/screens/post_options/index.ts | 4 +- .../post_options/options/edit_option.tsx | 32 +- app/screens/post_options/post_options.tsx | 33 +- assets/base/i18n/en.json | 17 +- 15 files changed, 722 insertions(+), 96 deletions(-) create mode 100644 app/screens/edit_post/edit_post.tsx create mode 100644 app/screens/edit_post/edit_post_input/index.tsx create mode 100644 app/screens/edit_post/index.tsx create mode 100644 app/screens/edit_post/post_error/index.tsx diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index 0ac390af0c..30e4234b89 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -667,6 +667,39 @@ export const markPostAsUnread = async (serverUrl: string, postId: string) => { } }; +export const editPost = async (serverUrl: string, postId: string, postMessage: string) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (!database) { + return {error: `${serverUrl} database not found`}; + } + let client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (error) { + return {error}; + } + + try { + const post = await queryPostById(database, postId); + if (post) { + const {update_at, edit_at, message: updatedMessage} = await client.patchPost({message: postMessage, id: postId}); + await database.write(async () => { + await post.update((p) => { + p.updateAt = update_at; + p.editAt = edit_at; + p.message = updatedMessage; + }); + }); + } + return { + post, + }; + } catch (error) { + forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); + return {error}; + } +}; + export async function fetchSavedPosts(serverUrl: string, teamId?: string, channelId?: string, page?: number, perPage?: number) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { diff --git a/app/components/autocomplete/autocomplete.tsx b/app/components/autocomplete/autocomplete.tsx index cb445712cd..7b01c5f4b6 100644 --- a/app/components/autocomplete/autocomplete.tsx +++ b/app/components/autocomplete/autocomplete.tsx @@ -56,6 +56,7 @@ type Props = { postInputTop: number; rootId: string; channelId: string; + fixedBottomPosition?: boolean; isSearch?: boolean; value: string; enableDateSuggestion?: boolean; @@ -63,6 +64,7 @@ type Props = { nestedScrollEnabled?: boolean; updateValue: (v: string) => void; hasFilesAttached: boolean; + maxHeightOverride?: number; } const Autocomplete = ({ @@ -72,7 +74,9 @@ const Autocomplete = ({ //channelId, isSearch = false, + fixedBottomPosition, value, + maxHeightOverride, //enableDateSuggestion = false, isAppsEnabled, @@ -97,6 +101,9 @@ const Autocomplete = ({ const appsTakeOver = false; // showingAppCommand; const maxListHeight = useMemo(() => { + if (maxHeightOverride) { + return maxHeightOverride; + } const isLandscape = dimensions.width > dimensions.height; const offset = isTablet && isLandscape ? OFFSET_TABLET : 0; let postInputDiff = 0; @@ -106,7 +113,7 @@ const Autocomplete = ({ postInputDiff = MAX_LIST_DIFF; } return MAX_LIST_HEIGHT - postInputDiff - offset; - }, [postInputTop, isTablet, dimensions.width]); + }, [maxHeightOverride, postInputTop, isTablet, dimensions.width]); const wrapperStyles = useMemo(() => { const s = []; @@ -124,9 +131,11 @@ const Autocomplete = ({ const containerStyles = useMemo(() => { const s = [style.borders]; - if (!isSearch) { + if (!isSearch && !fixedBottomPosition) { const offset = isTablet ? -OFFSET_TABLET : 0; s.push(style.base, {bottom: postInputTop + LIST_BOTTOM + offset}); + } else if (fixedBottomPosition) { + s.push(style.base, {bottom: 0}); } if (!hasElements) { s.push(style.hidden); diff --git a/app/components/error_text/index.tsx b/app/components/error_text/index.tsx index dc5363c261..3c8a1f8127 100644 --- a/app/components/error_text/index.tsx +++ b/app/components/error_text/index.tsx @@ -5,16 +5,17 @@ import React from 'react'; import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native'; import FormattedText from '@components/formatted_text'; +import {useTheme} from '@context/theme'; import {makeStyleSheetFromTheme} from '@utils/theme'; type ErrorProps = { error: ErrorText; testID?: string; textStyle?: StyleProp | StyleProp; - theme: Theme; } -const ErrorTextComponent = ({error, testID, textStyle, theme}: ErrorProps) => { +const ErrorTextComponent = ({error, testID, textStyle}: ErrorProps) => { + const theme = useTheme(); const style = getStyleSheet(theme); const message = typeof (error) === 'string' ? error : error.message; diff --git a/app/constants/screens.ts b/app/constants/screens.ts index 815d0ae26f..db43ef32d2 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -13,6 +13,7 @@ export const CHANNEL_DETAILS = 'ChannelDetails'; export const CHANNEL_EDIT = 'ChannelEdit'; export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter'; export const CUSTOM_STATUS = 'CustomStatus'; +export const EDIT_POST = 'EditPost'; export const EDIT_PROFILE = 'EditProfile'; export const EDIT_SERVER = 'EditServer'; export const FORGOT_PASSWORD = 'ForgotPassword'; @@ -47,6 +48,7 @@ export default { CHANNEL_DETAILS, CUSTOM_STATUS_CLEAR_AFTER, CUSTOM_STATUS, + EDIT_POST, EDIT_PROFILE, EDIT_SERVER, FORGOT_PASSWORD, diff --git a/app/screens/edit_post/edit_post.tsx b/app/screens/edit_post/edit_post.tsx new file mode 100644 index 0000000000..1e38502112 --- /dev/null +++ b/app/screens/edit_post/edit_post.tsx @@ -0,0 +1,308 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {useIntl} from 'react-intl'; +import {Alert, Keyboard, KeyboardType, LayoutChangeEvent, Platform, SafeAreaView, useWindowDimensions, View} from 'react-native'; +import {Navigation} from 'react-native-navigation'; +import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; + +import {deletePost, editPost} from '@actions/remote/post'; +import AutoComplete from '@components/autocomplete'; +import Loading from '@components/loading'; +import {useServerUrl} from '@context/server'; +import {useTheme} from '@context/theme'; +import {useIsTablet} from '@hooks/device'; +import useDidUpdate from '@hooks/did_update'; +import PostError from '@screens/edit_post/post_error'; +import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation'; +import {switchKeyboardForCodeBlocks} from '@utils/markdown'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import EditPostInput, {EditPostInputRef} from './edit_post_input'; + +import type PostModel from '@typings/database/models/servers/post'; + +const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { + return { + body: { + flex: 1, + backgroundColor: changeOpacity(theme.centerChannelColor, 0.03), + }, + container: { + flex: 1, + }, + loader: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + }; +}); + +const RIGHT_BUTTON = buildNavigationButton('edit-post', 'edit_post.save.button'); + +type EditPostProps = { + componentId: string; + closeButtonId: string; + post: PostModel; + maxPostSize: number; + hasFilesAttached: boolean; + canDelete: boolean; +} +const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttached, canDelete}: EditPostProps) => { + const [keyboardType, setKeyboardType] = useState('default'); + const [postMessage, setPostMessage] = useState(post.message); + const [cursorPosition, setCursorPosition] = useState(post.message.length); + const [errorLine, setErrorLine] = useState(); + const [errorExtra, setErrorExtra] = useState(); + const [isUpdating, setIsUpdating] = useState(false); + const layoutHeight = useSharedValue(0); + const keyboardHeight = useSharedValue(0); + + const postInputRef = useRef(null); + const theme = useTheme(); + const intl = useIntl(); + const serverUrl = useServerUrl(); + const isTablet = useIsTablet(); + const {width, height} = useWindowDimensions(); + const isLandscape = width > height; + const styles = getStyleSheet(theme); + + useEffect(() => { + setButtons(componentId, { + rightButtons: [{ + color: theme.sidebarHeaderTextColor, + text: intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'}), + ...RIGHT_BUTTON, + enabled: false, + }], + }); + }, []); + + useEffect(() => { + const showListener = Keyboard.addListener('keyboardWillShow', (e) => { + const {height: end} = e.endCoordinates; + + // on iPad if we use the hardware keyboard multiply its height by 2 + // otherwise use the software keyboard height + const minKeyboardHeight = end < 100 ? end * 2 : end; + keyboardHeight.value = minKeyboardHeight; + }); + const hideListener = Keyboard.addListener('keyboardWillHide', () => { + if (isTablet) { + const offset = isLandscape ? 60 : 0; + keyboardHeight.value = ((height - (layoutHeight.value + offset)) / 2); + return; + } + + keyboardHeight.value = 0; + }); + + return () => { + showListener.remove(); + hideListener.remove(); + }; + }, [isTablet, height]); + + useEffect(() => { + const t = setTimeout(() => { + postInputRef.current?.focus(); + }, 320); + + return () => { + clearTimeout(t); + }; + }, []); + + useEffect(() => { + const unsubscribe = Navigation.events().registerComponentListener({ + navigationButtonPressed: ({buttonId}: { buttonId: string }) => { + switch (buttonId) { + case closeButtonId: { + onClose(); + break; + } + case RIGHT_BUTTON.id: + onSavePostMessage(); + break; + } + }, + }, componentId); + + return () => { + unsubscribe.remove(); + }; + }, [postMessage]); + + useDidUpdate(() => { + // Workaround to avoid iOS emdash autocorrect in Code Blocks + if (Platform.OS === 'ios') { + onTextSelectionChange(); + } + }, [postMessage]); + + const onClose = useCallback(() => { + Keyboard.dismiss(); + dismissModal({componentId}); + }, []); + + const onLayout = useCallback((e: LayoutChangeEvent) => { + layoutHeight.value = e.nativeEvent.layout.height; + }, [height]); + + const onTextSelectionChange = useCallback((curPos: number = cursorPosition) => { + if (Platform.OS === 'ios') { + setKeyboardType(switchKeyboardForCodeBlocks(postMessage, curPos)); + } + setCursorPosition(curPos); + }, [cursorPosition, postMessage]); + + const toggleSaveButton = useCallback((enabled = true) => { + setButtons(componentId, { + rightButtons: [{ + ...RIGHT_BUTTON, + color: theme.sidebarHeaderTextColor, + text: intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'}), + enabled, + }], + }); + }, [componentId, intl, theme]); + + const onChangeText = useCallback((message: string) => { + setPostMessage(message); + const tooLong = message.trim().length > maxPostSize; + + if (tooLong) { + const line = intl.formatMessage({id: 'mobile.message_length.message_split_left', defaultMessage: 'Message exceeds the character limit'}); + const extra = `${message.trim().length} / ${maxPostSize}`; + setErrorLine(line); + setErrorExtra(extra); + } + toggleSaveButton(post.message !== message); + }, [intl, maxPostSize, toggleSaveButton]); + + const handleUIUpdates = useCallback((res) => { + if (res?.error) { + setIsUpdating(false); + const errorMessage = intl.formatMessage({id: 'mobile.edit_post.error', defaultMessage: 'There was a problem editing this message. Please try again.'}); + setErrorLine(errorMessage); + postInputRef?.current?.focus(); + } else { + setIsUpdating(false); + onClose(); + } + }, []); + + const handleDeletePost = useCallback(async () => { + Alert.alert( + intl.formatMessage({id: 'mobile.edit_post.delete_title', defaultMessage: 'Confirm Post Delete'}), + intl.formatMessage({ + id: 'mobile.edit_post.delete_question', + defaultMessage: 'Are you sure you want to delete this Post?', + }), + [{ + text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}), + style: 'cancel', + onPress: () => { + setIsUpdating(false); + toggleSaveButton(); + setPostMessage(post.message); + }, + }, { + text: intl.formatMessage({id: 'post_info.del', defaultMessage: 'Delete'}), + style: 'destructive', + onPress: async () => { + const res = await deletePost(serverUrl, post); + handleUIUpdates(res); + }, + }], + ); + }, [serverUrl, post.message]); + + const onSavePostMessage = useCallback(async () => { + setIsUpdating(true); + setErrorLine(undefined); + setErrorExtra(undefined); + toggleSaveButton(false); + if (!postMessage && canDelete && !hasFilesAttached) { + handleDeletePost(); + return; + } + + const res = await editPost(serverUrl, post.id, postMessage); + handleUIUpdates(res); + }, [toggleSaveButton, serverUrl, post.id, postMessage, onClose]); + + const animatedStyle = useAnimatedStyle(() => { + if (Platform.OS === 'android') { + return {bottom: 0}; + } + + let bottom = 0; + if (isTablet) { + // 60 is the size of the navigation header + const offset = isLandscape ? 60 : 0; + + bottom = keyboardHeight.value - ((height - (layoutHeight.value + offset)) / 2); + } else { + bottom = keyboardHeight.value; + } + + return { + bottom: withTiming(bottom, {duration: 250}), + }; + }); + + if (isUpdating) { + return ( + + + + ); + } + + return ( + <> + + + {Boolean((errorLine || errorExtra)) && + + } + + + + + + + + + ); +}; + +export default EditPost; diff --git a/app/screens/edit_post/edit_post_input/index.tsx b/app/screens/edit_post/edit_post_input/index.tsx new file mode 100644 index 0000000000..f720da9486 --- /dev/null +++ b/app/screens/edit_post/edit_post_input/index.tsx @@ -0,0 +1,93 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {forwardRef, useCallback, useImperativeHandle, useMemo, useRef} from 'react'; +import {useIntl} from 'react-intl'; +import {KeyboardType, Platform, TextInput, useWindowDimensions, View} from 'react-native'; + +import {useTheme} from '@context/theme'; +import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; + +const getStyleSheet = makeStyleSheetFromTheme((theme) => ({ + input: { + color: theme.centerChannelColor, + padding: 15, + textAlignVertical: 'top', + ...typography('Body', 200), + }, + inputContainer: { + backgroundColor: theme.centerChannelBg, + marginTop: 2, + }, +})); + +const HEIGHT_DIFF = Platform.select({android: 40, default: 30}); + +export type EditPostInputRef = { + focus: () => void; +} + +type PostInputProps = { + keyboardType: KeyboardType; + message: string; + hasError: boolean; + onTextSelectionChange: (curPos: number) => void; + onChangeText: (text: string) => void; +} + +const EditPostInput = forwardRef(({ + keyboardType, message, onChangeText, onTextSelectionChange, hasError, +}: PostInputProps, ref) => { + const intl = useIntl(); + const theme = useTheme(); + const styles = getStyleSheet(theme); + const {height} = useWindowDimensions(); + const textInputHeight = (height / 2) - HEIGHT_DIFF; + + const inputRef = useRef(null); + + const inputStyle = useMemo(() => { + return [styles.input, {height: textInputHeight}]; + }, [textInputHeight, styles]); + + const onSelectionChange = useCallback((event) => { + const curPos = event.nativeEvent.selection.end; + onTextSelectionChange(curPos); + }, [onTextSelectionChange]); + + const containerStyle = useMemo(() => [ + styles.inputContainer, + hasError && {marginTop: 0}, + {height: textInputHeight}, + ], [styles, textInputHeight]); + + useImperativeHandle(ref, () => ({ + focus: () => inputRef.current?.focus(), + }), [inputRef.current]); + + return ( + + + + ); +}); + +EditPostInput.displayName = 'EditPostInput'; + +export default EditPostInput; diff --git a/app/screens/edit_post/index.tsx b/app/screens/edit_post/index.tsx new file mode 100644 index 0000000000..341ce27a6a --- /dev/null +++ b/app/screens/edit_post/index.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider'; +import withObservables from '@nozbe/with-observables'; +import {of as of$} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; + +import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; +import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft'; + +import EditPost from './edit_post'; + +import type {WithDatabaseArgs} from '@typings/database/database'; +import type PostModel from '@typings/database/models/servers/post'; +import type SystemModel from '@typings/database/models/servers/system'; + +const enhance = withObservables([], ({database, post}: WithDatabaseArgs & { post: PostModel}) => { + const maxPostSize = database.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG).pipe( + switchMap(({value}) => of$(parseInt(value.MaxPostSize || '0', 10) || MAX_MESSAGE_LENGTH_FALLBACK)), + + ); + + const hasFilesAttached = post.files.observe().pipe(switchMap((files) => of$(files?.length > 0))); + + return { + maxPostSize, + hasFilesAttached, + }; +}); + +export default withDatabase(enhance(EditPost)); diff --git a/app/screens/edit_post/post_error/index.tsx b/app/screens/edit_post/post_error/index.tsx new file mode 100644 index 0000000000..999eb9f87b --- /dev/null +++ b/app/screens/edit_post/post_error/index.tsx @@ -0,0 +1,57 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {StyleSheet, View} from 'react-native'; + +import ErrorText from '@components/error_text'; + +type PostErrorProps = { + errorLine?: string; + errorExtra?: string; +} + +const styles = StyleSheet.create({ + errorContainerSplit: { + paddingHorizontal: 15, + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + }, + errorContainer: { + paddingHorizontal: 10, + width: '100%', + }, + errorWrap: { + flexShrink: 1, + paddingRight: 20, + }, + errorWrapper: { + alignItems: 'center', + }, +}); + +const PostError = ({errorLine, errorExtra}: PostErrorProps) => { + return ( + + {Boolean(errorLine) && ( + + )} + {Boolean(errorExtra) && ( + + )} + + ); +}; + +export default PostError; diff --git a/app/screens/edit_profile/components/profile_error.tsx b/app/screens/edit_profile/components/profile_error.tsx index f6283ead8d..b8a1742baf 100644 --- a/app/screens/edit_profile/components/profile_error.tsx +++ b/app/screens/edit_profile/components/profile_error.tsx @@ -48,7 +48,6 @@ const ProfileError = ({error}: DisplayErrorProps) => { name='alert-outline' /> ) => { return function gestureHoc(props: any) { if (Platform.OS === 'android') { @@ -23,11 +20,11 @@ const withGestures = (Screen: React.ComponentType, styles: StyleProp) - ) + ); } return ; - } + }; }; const withIntl = (Screen: React.ComponentType) => { @@ -39,86 +36,103 @@ const withIntl = (Screen: React.ComponentType) => { > - ); - } -} + ); + }; +}; const withSafeAreaInsets = (Screen: React.ComponentType) => { - return function SafeAreaInsets(props: any){ + return function SafeAreaInsets(props: any) { return ( - + - ) - } -} + ); + }; +}; Navigation.setLazyComponentRegistrator((screenName) => { let screen: any|undefined; let extraStyles: StyleProp; switch (screenName) { - case Screens.ABOUT: - screen = withServerDatabase(require('@screens/about').default); - break; - case Screens.BOTTOM_SHEET: - screen = withServerDatabase(require('@screens/bottom_sheet').default); - break; - case Screens.CHANNEL: - screen = withServerDatabase(require('@screens/channel').default); - break; - case Screens.CUSTOM_STATUS: - screen = withServerDatabase(require('@screens/custom_status').default); - break; - case Screens.CUSTOM_STATUS_CLEAR_AFTER: - screen = withServerDatabase(require('@screens/custom_status_clear_after').default); - break; - case Screens.EMOJI_PICKER: - screen = withServerDatabase(require('@screens/emoji_picker').default); - break; - case Screens.EDIT_PROFILE: - screen = withServerDatabase((require('@screens/edit_profile').default)); - break; - case Screens.EDIT_SERVER: - screen = withIntl(require('@screens/edit_server').default); - break; - case Screens.FORGOT_PASSWORD: - screen = withIntl(require('@screens/forgot_password').default); - break; - case Screens.GALLERY: - screen = withServerDatabase((require('@screens/gallery').default)); - break; - case Screens.IN_APP_NOTIFICATION: { - const notificationScreen = require('@screens/in_app_notification').default; - Navigation.registerComponent(Screens.IN_APP_NOTIFICATION, () => Platform.select({ - default: notificationScreen, - ios: withSafeAreaInsets(notificationScreen), - })); - return; - } - case Screens.LOGIN: - screen = withIntl(require('@screens/login').default); - break; - case Screens.MFA: - screen = withIntl(require('@screens/mfa').default); - break; - case Screens.BROWSE_CHANNELS: - screen = withServerDatabase(require('@screens/browse_channels').default); - break; - case Screens.POST_OPTIONS: - screen = withServerDatabase(require('@screens/post_options').default); - break; - case Screens.SSO: - screen = withIntl(require('@screens/sso').default); - break; - case Screens.SAVED_POSTS: - screen = withServerDatabase((require('@screens/home/saved_posts').default)); - break; - case Screens.CREATE_DIRECT_MESSAGE: - screen = withServerDatabase((require('@screens/create_direct_message').default)); - break; - case Screens.THREAD: - screen = withServerDatabase(require('@screens/thread').default); - break; + case Screens.ABOUT: + screen = withServerDatabase(require('@screens/about').default); + break; + case Screens.BOTTOM_SHEET: + screen = withServerDatabase( + require('@screens/bottom_sheet').default, + ); + break; + case Screens.CHANNEL: + screen = withServerDatabase(require('@screens/channel').default); + break; + case Screens.CUSTOM_STATUS: + screen = withServerDatabase( + require('@screens/custom_status').default, + ); + break; + case Screens.CUSTOM_STATUS_CLEAR_AFTER: + screen = withServerDatabase( + require('@screens/custom_status_clear_after').default, + ); + break; + case Screens.EDIT_POST: + screen = withServerDatabase(require('@screens/edit_post').default); + break; + case Screens.EDIT_PROFILE: + screen = withServerDatabase( + require('@screens/edit_profile').default, + ); + break; + case Screens.EDIT_SERVER: + screen = withIntl(require('@screens/edit_server').default); + break; + case Screens.EMOJI_PICKER: + screen = withServerDatabase( + require('@screens/emoji_picker').default, + ); + break; + case Screens.FORGOT_PASSWORD: + screen = withIntl(require('@screens/forgot_password').default); + break; + case Screens.GALLERY: + screen = withServerDatabase(require('@screens/gallery').default); + break; + case Screens.IN_APP_NOTIFICATION: { + const notificationScreen = + require('@screens/in_app_notification').default; + Navigation.registerComponent(Screens.IN_APP_NOTIFICATION, () => + Platform.select({ + default: notificationScreen, + ios: withSafeAreaInsets(notificationScreen), + }), + ); + return; + } + case Screens.LOGIN: + screen = withIntl(require('@screens/login').default); + break; + case Screens.MFA: + screen = withIntl(require('@screens/mfa').default); + break; + case Screens.BROWSE_CHANNELS: + screen = withServerDatabase( + require('@screens/browse_channels').default, + ); + break; + case Screens.POST_OPTIONS: + screen = withServerDatabase( + require('@screens/post_options').default, + ); + break; + case Screens.SAVED_POSTS: + screen = withServerDatabase((require('@screens/home/saved_posts').default)); + break; + case Screens.SSO: + screen = withIntl(require('@screens/sso').default); + break; + case Screens.THREAD: + screen = withServerDatabase(require('@screens/thread').default); + break; } if (screen) { diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index b0dc562f0b..df5c965ea1 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -5,7 +5,7 @@ import merge from 'deepmerge'; import {Appearance, DeviceEventEmitter, NativeModules, StatusBar, Platform, Alert} from 'react-native'; -import {Navigation, Options, OptionsModalPresentationStyle} from 'react-native-navigation'; +import {ImageResource, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton} from 'react-native-navigation'; import tinyColor from 'tinycolor2'; import CompassIcon from '@components/compass_icon'; @@ -105,6 +105,18 @@ Navigation.setDefaultOptions({ layout: { orientation: Device.IS_TABLET ? undefined : ['portrait'], }, + topBar: { + title: { + fontFamily: 'Metropolis-SemiBold', + fontSize: 18, + fontWeight: '600', + }, + subtitle: { + fontFamily: 'OpenSans', + fontSize: 12, + fontWeight: '500', + }, + }, }); Appearance.addChangeListener(() => { @@ -556,6 +568,16 @@ export async function dismissAllModals(options: Options = {}) { } } +export const buildNavigationButton = (id: string, testID: string, icon?: ImageResource): OptionsTopBarButton => ({ + fontSize: 16, + fontFamily: 'OpenSans-SemiBold', + fontWeight: '600', + id, + icon, + showAsAction: 'always', + testID, +}); + export function setButtons(componentId: string, buttons: NavButtons = {leftButtons: [], rightButtons: []}) { const options = { topBar: { diff --git a/app/screens/post_options/index.ts b/app/screens/post_options/index.ts index 110cbd58f6..74999ce95c 100644 --- a/app/screens/post_options/index.ts +++ b/app/screens/post_options/index.ts @@ -122,8 +122,8 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, loca const canEdit = combineLatest([postEditTimeLimit, isLicensed, channel, currentUser, channelIsArchived, channelIsReadOnly, canEditUntil, canPostPermission]).pipe(switchMap(([lt, ls, c, u, isArchived, isReadOnly, until, canPost]) => { const isOwner = u.id === post.userId; const canEditPostPermission = canEditPost(isOwner, post, lt, ls, c, u); - const timeReached = until === -1 || until > Date.now(); - return of$(canEditPostPermission && isSystemMessage(post) && !isArchived && !isReadOnly && !timeReached && canPost); + const timeNotReached = (until === -1) || (until > Date.now()); + return of$(canEditPostPermission && !isArchived && !isReadOnly && timeNotReached && canPost); })); const canMarkAsUnread = combineLatest([currentUser, channelIsArchived]).pipe( diff --git a/app/screens/post_options/options/edit_option.tsx b/app/screens/post_options/options/edit_option.tsx index f3ccd97eaa..a87aea1c98 100644 --- a/app/screens/post_options/options/edit_option.tsx +++ b/app/screens/post_options/options/edit_option.tsx @@ -2,22 +2,44 @@ // See LICENSE.txt for license information. import React, {useCallback} from 'react'; +import {useIntl} from 'react-intl'; +import CompassIcon from '@components/compass_icon'; import {Screens} from '@constants'; +import {useTheme} from '@context/theme'; import {t} from '@i18n'; -import {dismissBottomSheet, goToScreen} from '@screens/navigation'; -import PostModel from '@typings/database/models/servers/post'; +import {dismissBottomSheet, showModal} from '@screens/navigation'; import BaseOption from './base_option'; +import type PostModel from '@typings/database/models/servers/post'; + type Props = { post: PostModel; + canDelete: boolean; } -const EditOption = ({post}: Props) => { +const EditOption = ({post, canDelete}: Props) => { + const intl = useIntl(); + const theme = useTheme(); + const onPress = useCallback(async () => { - // https://mattermost.atlassian.net/browse/MM-41991 await dismissBottomSheet(Screens.POST_OPTIONS); - goToScreen('EDIT_SCREEN_NOT_IMPLEMENTED_YET', '', {post}); + + const title = intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}); + const closeButton = CompassIcon.getImageSourceSync('close', 24, theme.sidebarHeaderTextColor); + const closeButtonId = 'close-edit-post'; + const passProps = {post, closeButtonId, canDelete}; + const options = { + modal: {swipeToDismiss: false}, + topBar: { + leftButtons: [{ + id: closeButtonId, + testID: 'close.edit_post.button', + icon: closeButton, + }], + }, + }; + showModal(Screens.EDIT_POST, title, passProps, options); }, [post]); return ( diff --git a/app/screens/post_options/post_options.tsx b/app/screens/post_options/post_options.tsx index e465a8d20d..b6fd8ca65a 100644 --- a/app/screens/post_options/post_options.tsx +++ b/app/screens/post_options/post_options.tsx @@ -2,11 +2,13 @@ // See LICENSE.txt for license information. import {useManagedConfig} from '@mattermost/react-native-emm'; -import React from 'react'; +import React, {useEffect} from 'react'; +import {Navigation} from 'react-native-navigation'; import {ITEM_HEIGHT} from '@components/menu_item'; import {Screens} from '@constants'; import BottomSheet from '@screens/bottom_sheet'; +import {dismissModal} from '@screens/navigation'; import {isSystemMessage} from '@utils/post'; import CopyLinkOption from './options/copy_permalink_option'; @@ -34,6 +36,7 @@ type PostOptionsProps = { location: typeof Screens[keyof typeof Screens]; post: PostModel; thread: Partial; + componentId: string; }; const PostOptions = ({ @@ -44,12 +47,31 @@ const PostOptions = ({ canPin, canReply, combinedPost, + componentId, isSaved, location, post, thread, }: PostOptionsProps) => { const managedConfig = useManagedConfig(); + + useEffect(() => { + const unsubscribe = Navigation.events().registerComponentListener({ + navigationButtonPressed: ({buttonId}: { buttonId: string }) => { + switch (buttonId) { + case 'close-post-options': { + dismissModal({componentId}); + break; + } + } + }, + }, componentId); + + return () => { + unsubscribe.remove(); + }; + }, []); + const isSystemPost = isSystemMessage(post); const canCopyPermalink = !isSystemPost && managedConfig?.copyAndPasteProtection !== 'true'; @@ -86,14 +108,19 @@ const PostOptions = ({ postId={post.id} /> } - {canCopyText && } + {Boolean(canCopyText && post.message) && } {canPin && } - {canEdit && } + {canEdit && + + } {canDelete &&