From 3ef280c80cf25120e6df6c3920abfa4a9559d2cb Mon Sep 17 00:00:00 2001 From: Avinash Lingaloo Date: Tue, 19 Apr 2022 16:10:37 +0400 Subject: [PATCH] styling mobile wip --- app/components/post_list/index.tsx | 22 +++- app/components/post_list/post/post.tsx | 5 +- app/components/toast/index.tsx | 10 +- app/constants/index.ts | 6 +- app/constants/screens.ts | 2 + app/constants/snack_bar.ts | 56 +++++++++ app/screens/index.tsx | 13 +- .../copy_permalink_option.tsx | 13 +- .../post_options/options/copy_text_option.tsx | 11 +- app/screens/post_options/post_options.tsx | 31 ++--- app/screens/snack_bar/index.tsx | 113 ++++++++++++++++++ app/utils/snack_bar/index.ts | 16 +++ 12 files changed, 266 insertions(+), 32 deletions(-) create mode 100644 app/constants/snack_bar.ts create mode 100644 app/screens/snack_bar/index.tsx create mode 100644 app/utils/snack_bar/index.ts diff --git a/app/components/post_list/index.tsx b/app/components/post_list/index.tsx index a4fc270af6..0b93dcf2db 100644 --- a/app/components/post_list/index.tsx +++ b/app/components/post_list/index.tsx @@ -3,7 +3,17 @@ import {FlatList} from '@stream-io/flat-list-mvcp'; import React, {ReactElement, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter, NativeScrollEvent, NativeSyntheticEvent, Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native'; +import { + DeviceEventEmitter, + LayoutChangeEvent, + NativeScrollEvent, + NativeSyntheticEvent, + Platform, + StyleProp, + StyleSheet, + View, + ViewStyle, +} from 'react-native'; import Animated from 'react-native-reanimated'; import {fetchPosts, fetchPostThread} from '@actions/remote/post'; @@ -111,6 +121,12 @@ const PostList = ({ return orderedPosts.indexOf(START_OF_NEW_MESSAGES); }, [orderedPosts]); + const [offSetY, setOffSetY] = useState(0); + const onLayout = useCallback((layoutEvent: LayoutChangeEvent) => { + const {layout} = layoutEvent.nativeEvent; + setOffSetY(layout.y); + }, []); + useEffect(() => { listRef.current?.scrollToOffset({offset: 0, animated: false}); }, [channelId]); @@ -296,6 +312,7 @@ const PostList = ({ previousPost, shouldRenderReplyButton, skipSaveddHeader, + offSetY, }; return ( @@ -307,7 +324,7 @@ const PostList = ({ {...postProps} /> ); - }, [currentTimezone, highlightPinnedOrSaved, isTimezoneEnabled, orderedPosts, shouldRenderReplyButton, theme]); + }, [currentTimezone, highlightPinnedOrSaved, isTimezoneEnabled, orderedPosts, shouldRenderReplyButton, theme, offSetY]); const scrollToIndex = useCallback((index: number, animated = true, applyOffset = true) => { listRef.current?.scrollToIndex({ @@ -366,6 +383,7 @@ const PostList = ({ testID={`${testID}.flat_list`} /> + {showMoreMessages && ; testID?: string; + offSetY: number; }; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { @@ -99,7 +100,7 @@ const Post = ({ appsEnabled, canDelete, currentUser, differentThreadSequence, filesCount, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar, isConsecutivePost, isEphemeral, isFirstReply, isSaved, isJumboEmoji, isLastReply, isPostAddChannelMember, location, post, reactionsCount, shouldRenderReplyButton, skipSavedHeader, skipPinnedHeader, showAddReaction = true, style, - testID, previousPost, + testID, previousPost, offSetY, }: PostProps) => { const pressDetected = useRef(false); const intl = useIntl(); @@ -166,7 +167,7 @@ const Post = ({ } Keyboard.dismiss(); - const passProps = {location, post, showAddReaction}; + const passProps = {location, post, showAddReaction, offSetY}; const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : ''; if (isTablet) { diff --git a/app/components/toast/index.tsx b/app/components/toast/index.tsx index b4da4dbc24..8218a9ff1d 100644 --- a/app/components/toast/index.tsx +++ b/app/components/toast/index.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useMemo} from 'react'; -import {StyleProp, Text, useWindowDimensions, View, ViewStyle} from 'react-native'; +import {StyleProp, Text, TextStyle, useWindowDimensions, View, ViewStyle} from 'react-native'; import Animated, {AnimatedStyleProp} from 'react-native-reanimated'; import CompassIcon from '@components/compass_icon'; @@ -15,7 +15,8 @@ type ToastProps = { children?: React.ReactNode; iconName?: string; message?: string; - style: StyleProp; + style?: StyleProp; + textStyle?: StyleProp; } const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ @@ -46,7 +47,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ }, })); -const Toast = ({animatedStyle, children, style, iconName, message}: ToastProps) => { +const Toast = ({animatedStyle, children, style, iconName, message, textStyle}: ToastProps) => { const theme = useTheme(); const styles = getStyleSheet(theme); const dim = useWindowDimensions(); @@ -65,11 +66,12 @@ const Toast = ({animatedStyle, children, style, iconName, message}: ToastProps) color={theme.buttonColor} name={iconName!} size={18} + style={textStyle} /> } {Boolean(message) && - {message} + {message} } {children} diff --git a/app/constants/index.ts b/app/constants/index.ts index 1f06991488..4a71cfdd2e 100644 --- a/app/constants/index.ts +++ b/app/constants/index.ts @@ -24,6 +24,7 @@ import Preferences from './preferences'; import Profile from './profile'; import Screens from './screens'; import ServerErrors from './server_errors'; +import SnackBar from './snack_bar'; import Sso from './sso'; import SupportedServer from './supported_server'; import View from './view'; @@ -33,9 +34,9 @@ export { ActionType, Apps, Categories, + Channel, Config, CustomStatusDuration, - Channel, Database, DeepLink, Device, @@ -53,8 +54,9 @@ export { Profile, Screens, ServerErrors, - SupportedServer, + SnackBar, Sso, + SupportedServer, View, WebsocketEvents, }; diff --git a/app/constants/screens.ts b/app/constants/screens.ts index 196448a867..7ce2c24bbf 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -39,6 +39,7 @@ export const SSO = 'SSO'; export const THREAD = 'Thread'; export const THREAD_FOLLOW_BUTTON = 'ThreadFollowButton'; export const USER_PROFILE = 'UserProfile'; +export const SNACK_BAR = 'SnackBar'; export default { ABOUT, @@ -79,6 +80,7 @@ export default { THREAD, THREAD_FOLLOW_BUTTON, USER_PROFILE, + SNACK_BAR, }; export const MODAL_SCREENS_WITHOUT_BACK = [ diff --git a/app/constants/snack_bar.ts b/app/constants/snack_bar.ts new file mode 100644 index 0000000000..7ace81f051 --- /dev/null +++ b/app/constants/snack_bar.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {t} from '@i18n'; +import keyMirror from '@utils/key_mirror'; + +export const SNACK_BAR_TYPE = keyMirror({ + LINK_COPIED: null, + MESSAGE_COPIED: null, + FOLLOW_THREAD: null, + MUTE_CHANNEL: null, + FAILED_TO_SAVE_MESSAGE: null, +}); + +type SnackBarConfig = { + id: string; + defaultMessage: string; + iconName: string; + themeColor: string; + canUndo: boolean; +} +export const SNACK_BAR_CONFIG: Record = { + LINK_COPIED: { + id: t('snack.bar.link.copied'), + defaultMessage: 'Link copied to clipboard', + iconName: 'check', + themeColor: 'onlineIndicator', + canUndo: false, + }, + MESSAGE_COPIED: { + id: t('snack.bar.message.copied'), + defaultMessage: 'Message copied to clipboard', + iconName: 'content-copy', + themeColor: 'centerChannelColor', + canUndo: false, + }, + FOLLOW_THREAD: { + id: t('snack.bar.follow.thread'), + defaultMessage: 'You\'re now following this thread', + iconName: 'message-check-outline', + themeColor: 'centerChannelColor', + canUndo: true, + }, + MUTE_CHANNEL: { + id: t('snack.bar.mute.channel'), + defaultMessage: 'This channel was muted', + iconName: 'bell-off-outline', + themeColor: 'centerChannelColor', + canUndo: true, + }, +}; + +export default { + SNACK_BAR_TYPE, + SNACK_BAR_CONFIG, +}; diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 367de5d6ba..e46193d418 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import {withManagedConfig} from '@mattermost/react-native-emm'; -import React from 'react'; +import React, {ComponentType} from 'react'; import {IntlProvider} from 'react-intl'; import {Platform, StyleProp, ViewStyle} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; @@ -153,6 +153,17 @@ Navigation.setLazyComponentRegistrator((screenName) => { require('@screens/thread/thread_follow_button').default, )); break; + case Screens.SNACK_BAR: { + const snackBarScreen = withServerDatabase(require('@screens/snack_bar').default); + + Navigation.registerComponent(Screens.SNACK_BAR, () => + Platform.select({ + default: snackBarScreen, + ios: withSafeAreaInsets(snackBarScreen) as ComponentType, + }), + ); + break; + } } if (screen) { diff --git a/app/screens/post_options/options/copy_permalink_option/copy_permalink_option.tsx b/app/screens/post_options/options/copy_permalink_option/copy_permalink_option.tsx index 8528709dfe..9f58f31da9 100644 --- a/app/screens/post_options/options/copy_permalink_option/copy_permalink_option.tsx +++ b/app/screens/post_options/options/copy_permalink_option/copy_permalink_option.tsx @@ -5,25 +5,30 @@ import Clipboard from '@react-native-community/clipboard'; import React, {useCallback} from 'react'; import {Screens} from '@constants'; +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'; import BaseOption from '../base_option'; import type PostModel from '@typings/database/models/servers/post'; type Props = { - teamName: string; + location: typeof Screens[keyof typeof Screens]; post: PostModel; + teamName: string; + offSetY: number; } -const CopyPermalinkOption = ({teamName, post}: Props) => { +const CopyPermalinkOption = ({teamName, post, location, offSetY}: Props) => { const serverUrl = useServerUrl(); - const handleCopyLink = useCallback(() => { + const handleCopyLink = useCallback(async () => { const permalink = `${serverUrl}/${teamName}/pl/${post.id}`; Clipboard.setString(permalink); - dismissBottomSheet(Screens.POST_OPTIONS); + await dismissBottomSheet(Screens.POST_OPTIONS); + showSnackBar({barType: SNACK_BAR_TYPE.LINK_COPIED, location, offSetY}); }, [teamName, post.id]); return ( diff --git a/app/screens/post_options/options/copy_text_option.tsx b/app/screens/post_options/options/copy_text_option.tsx index ade8fb8474..a26e1088ec 100644 --- a/app/screens/post_options/options/copy_text_option.tsx +++ b/app/screens/post_options/options/copy_text_option.tsx @@ -5,18 +5,23 @@ import Clipboard from '@react-native-community/clipboard'; import React, {useCallback} from 'react'; import {Screens} from '@constants'; +import {SNACK_BAR_TYPE} from '@constants/snack_bar'; import {t} from '@i18n'; import {dismissBottomSheet} from '@screens/navigation'; +import {showSnackBar} from '@utils/snack_bar'; import BaseOption from './base_option'; type Props = { + location: typeof Screens[keyof typeof Screens]; postMessage: string; + offSetY: number; } -const CopyTextOption = ({postMessage}: Props) => { - const handleCopyText = useCallback(() => { +const CopyTextOption = ({postMessage, location, offSetY}: Props) => { + const handleCopyText = useCallback(async () => { Clipboard.setString(postMessage); - dismissBottomSheet(Screens.POST_OPTIONS); + await dismissBottomSheet(Screens.POST_OPTIONS); + showSnackBar({barType: SNACK_BAR_TYPE.MESSAGE_COPIED, location, offSetY}); }, [postMessage]); return ( diff --git a/app/screens/post_options/post_options.tsx b/app/screens/post_options/post_options.tsx index 48c5749ac4..53aa8f65f0 100644 --- a/app/screens/post_options/post_options.tsx +++ b/app/screens/post_options/post_options.tsx @@ -37,21 +37,14 @@ type PostOptionsProps = { post: PostModel; thread: Partial; componentId: string; + offSetY: number; }; const PostOptions = ({ - canAddReaction, - canDelete, - canEdit, - canMarkAsUnread, - canPin, - canReply, - combinedPost, - componentId, - isSaved, - location, - post, - thread, + canAddReaction, canDelete, canEdit, + canMarkAsUnread, canPin, canReply, + combinedPost, componentId, isSaved, + location, post, thread, offSetY, }: PostOptionsProps) => { const managedConfig = useManagedConfig(); @@ -101,14 +94,24 @@ const PostOptions = ({ {canMarkAsUnread && !isSystemPost && } - {canCopyPermalink && } + {Boolean(canCopyPermalink && post.message) && + } {!isSystemPost && } - {Boolean(canCopyText && post.message) && } + {Boolean(canCopyText && post.message) && + } {canPin && { + return { + text: { + color: theme.centerChannelBg, + }, + undo: { + color: theme.centerChannelBg, + ...typography('Body', 100, 'SemiBold'), + }, + }; +}); + +type SnackBarProps = { + componentId: string; + onPress?: () => void; + barType: keyof typeof SNACK_BAR_TYPE; + location: typeof Screens[keyof typeof Screens]; +} + +const SnackBar = ({barType, componentId, onPress, location}: SnackBarProps) => { + const [showToast, setShowToast] = useState(); + const intl = useIntl(); + const theme = useTheme(); + const isTablet = useIsTablet(); + + const config = SNACK_BAR_CONFIG[barType]; + const styles = getStyleSheet(theme); + + const onPressHandler = useCallback(() => { + dismissOverlay(componentId); + onPress?.(); + }, [onPress, componentId]); + + const animatedStyle = useAnimatedStyle(() => { + let diff = 50; + const screens = [Screens.PERMALINK, Screens.MENTIONS, Screens.SAVED_POSTS]; + if (!isTablet && screens.includes(location)) { + diff = 7; + } + + return { + position: 'absolute', + bottom: BOTTOM_TAB_HEIGHT + diff, + opacity: withTiming(showToast ? 1 : 0, {duration: 300}), + }; + }); + + useEffect(() => { + setShowToast(true); + const t = setTimeout(() => { + setShowToast(false); + }, 3000); + + return () => clearTimeout(t); + }, []); + + useEffect(() => { + let t: NodeJS.Timeout; + if (showToast === false) { + t = setTimeout(() => { + dismissOverlay(componentId); + }, 350); + } + + return () => { + if (t) { + clearTimeout(t); + } + }; + }, [showToast]); + + return ( + + {config.canUndo && onPress && ( + + + {intl.formatMessage({ + id: 'snack.bar.undo', + defaultMessage: 'Undo', + })} + + )} + + ); +}; + +export default SnackBar; diff --git a/app/utils/snack_bar/index.ts b/app/utils/snack_bar/index.ts new file mode 100644 index 0000000000..409d17ffc0 --- /dev/null +++ b/app/utils/snack_bar/index.ts @@ -0,0 +1,16 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {Screens} from '@constants'; +import {SNACK_BAR_TYPE} from '@constants/snack_bar'; +import {showOverlay} from '@screens/navigation'; + +type ShowSnackBarArgs = { + barType: keyof typeof SNACK_BAR_TYPE; + onPress?: () => void; + location?: typeof Screens[keyof typeof Screens]; + offSetY?: number; +}; +export const showSnackBar = (passProps: ShowSnackBarArgs) => { + const screen = Screens.SNACK_BAR; + showOverlay(screen, passProps); +};