forked from Ivasoft/mattermost-mobile
MM-41532 Gekidou Snack Bars (#6181)
* styling mobile wip * styling tablet wip tablet portrait * removed offSetY * Update en.json * corrections from reviews * removed space * changed location to sourceScreen in post.tsx * adjust width * update message to text * adding PanGesture adding PanGesture- wip PanGesture- wip * able to touch through on iOS * using EphemeralStore and listeners to discard overlays * snack positioning and touches * PanGesture- wip * PanGesture- fine tuning the animation PanGesture- wip * removed toast keyword * checks for ongoing animation * dismiss overlay on navigating away * dismiss overlay on navigating away * dismiss overlay on tabPress * dismiss overlay on tabPress * cancelled timers on panning start * add entering layoutAnimation * add exitingg layoutAnimation * fix layoutAnimation * fix styling * eslint fix * style fix * fix timer not stopping * revert changes made to the ephemeral store * bumping the toast vertically by 4px Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
@@ -6,23 +6,27 @@ import React, {useCallback} from 'react';
|
||||
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
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 type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
teamName: string;
|
||||
sourceScreen: typeof Screens[keyof typeof Screens];
|
||||
post: PostModel;
|
||||
teamName: string;
|
||||
}
|
||||
const CopyPermalinkOption = ({teamName, post}: Props) => {
|
||||
const CopyPermalinkOption = ({teamName, post, sourceScreen}: 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, sourceScreen});
|
||||
}, [teamName, post.id]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -171,7 +171,7 @@ const Post = ({
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
const passProps = {location, post, showAddReaction};
|
||||
const passProps = {sourceScreen: location, post, showAddReaction};
|
||||
const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : '';
|
||||
|
||||
if (isTablet) {
|
||||
|
||||
@@ -74,7 +74,7 @@ const ThreadOverview = ({isSaved, repliesCount, rootPost, style, testID}: Props)
|
||||
const showPostOptions = useCallback(preventDoubleTap(() => {
|
||||
Keyboard.dismiss();
|
||||
if (rootPost?.id) {
|
||||
const passProps = {location: Screens.THREAD, post: rootPost, showAddReaction: true};
|
||||
const passProps = {sourceScreen: Screens.THREAD, post: rootPost, showAddReaction: true};
|
||||
const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : '';
|
||||
|
||||
if (isTablet) {
|
||||
|
||||
@@ -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,9 +15,12 @@ type ToastProps = {
|
||||
children?: React.ReactNode;
|
||||
iconName?: string;
|
||||
message?: string;
|
||||
style: StyleProp<ViewStyle>;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
export const TOAST_HEIGHT = 56;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
center: {
|
||||
alignItems: 'center',
|
||||
@@ -31,7 +34,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
elevation: 6,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 56,
|
||||
height: TOAST_HEIGHT,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 10,
|
||||
shadowColor: changeOpacity('#000', 0.12),
|
||||
@@ -46,7 +49,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 +68,12 @@ const Toast = ({animatedStyle, children, style, iconName, message}: ToastProps)
|
||||
color={theme.buttonColor}
|
||||
name={iconName!}
|
||||
size={18}
|
||||
style={textStyle}
|
||||
/>
|
||||
}
|
||||
{Boolean(message) &&
|
||||
<View style={styles.flex}>
|
||||
<Text style={styles.text}>{message}</Text>
|
||||
<Text style={[styles.text, textStyle]}>{message}</Text>
|
||||
</View>
|
||||
}
|
||||
{children}
|
||||
|
||||
@@ -26,6 +26,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';
|
||||
@@ -35,9 +36,9 @@ export {
|
||||
ActionType,
|
||||
Apps,
|
||||
Categories,
|
||||
Channel,
|
||||
Config,
|
||||
CustomStatusDuration,
|
||||
Channel,
|
||||
Database,
|
||||
DateTime,
|
||||
DeepLink,
|
||||
@@ -57,8 +58,9 @@ export {
|
||||
Profile,
|
||||
Screens,
|
||||
ServerErrors,
|
||||
SupportedServer,
|
||||
SnackBar,
|
||||
Sso,
|
||||
SupportedServer,
|
||||
View,
|
||||
WebsocketEvents,
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ export const THREAD = 'Thread';
|
||||
export const THREAD_FOLLOW_BUTTON = 'ThreadFollowButton';
|
||||
export const THREAD_OPTIONS = 'ThreadOptions';
|
||||
export const USER_PROFILE = 'UserProfile';
|
||||
export const SNACK_BAR = 'SnackBar';
|
||||
|
||||
export default {
|
||||
ABOUT,
|
||||
@@ -93,6 +94,7 @@ export default {
|
||||
THREAD_FOLLOW_BUTTON,
|
||||
THREAD_OPTIONS,
|
||||
USER_PROFILE,
|
||||
SNACK_BAR,
|
||||
};
|
||||
|
||||
export const MODAL_SCREENS_WITHOUT_BACK = [
|
||||
|
||||
51
app/constants/snack_bar.ts
Normal file
51
app/constants/snack_bar.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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;
|
||||
canUndo: boolean;
|
||||
}
|
||||
export const SNACK_BAR_CONFIG: Record<string, SnackBarConfig> = {
|
||||
LINK_COPIED: {
|
||||
id: t('snack.bar.link.copied'),
|
||||
defaultMessage: 'Link copied to clipboard',
|
||||
iconName: 'link-variant',
|
||||
canUndo: false,
|
||||
},
|
||||
MESSAGE_COPIED: {
|
||||
id: t('snack.bar.message.copied'),
|
||||
defaultMessage: 'Text copied to clipboard',
|
||||
iconName: 'content-copy',
|
||||
canUndo: false,
|
||||
},
|
||||
FOLLOW_THREAD: {
|
||||
id: t('snack.bar.follow.thread'),
|
||||
defaultMessage: 'You\'re now following this thread',
|
||||
iconName: 'message-check-outline',
|
||||
canUndo: true,
|
||||
},
|
||||
MUTE_CHANNEL: {
|
||||
id: t('snack.bar.mute.channel'),
|
||||
defaultMessage: 'This channel was muted',
|
||||
iconName: 'bell-off-outline',
|
||||
canUndo: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
SNACK_BAR_TYPE,
|
||||
SNACK_BAR_CONFIG,
|
||||
};
|
||||
@@ -162,7 +162,7 @@ function TabBar({state, descriptors, navigation, theme}: BottomTabBarProps & {th
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
DeviceEventEmitter.emit('tabPress');
|
||||
if (!isFocused && !event.defaultPrevented) {
|
||||
// The `merge: true` option makes sure that the params inside the tab screen are preserved
|
||||
navigation.navigate({params: {direction}, name: route.name, merge: false});
|
||||
|
||||
@@ -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';
|
||||
@@ -169,6 +169,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;
|
||||
}
|
||||
case Screens.THREAD_OPTIONS:
|
||||
screen = withServerDatabase(
|
||||
require('@screens/thread_options').default,
|
||||
|
||||
@@ -629,7 +629,10 @@ export function showOverlay(name: string, passProps = {}, options = {}) {
|
||||
component: {
|
||||
id: name,
|
||||
name,
|
||||
passProps,
|
||||
passProps: {
|
||||
...passProps,
|
||||
overlay: true,
|
||||
},
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,16 +6,20 @@ import React, {useCallback} from 'react';
|
||||
|
||||
import {BaseOption} from '@components/common_post_options';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
sourceScreen: typeof Screens[keyof typeof Screens];
|
||||
postMessage: string;
|
||||
}
|
||||
const CopyTextOption = ({postMessage}: Props) => {
|
||||
const CopyTextOption = ({postMessage, sourceScreen}: Props) => {
|
||||
const handleCopyText = useCallback(async () => {
|
||||
await dismissBottomSheet(Screens.POST_OPTIONS);
|
||||
Clipboard.setString(postMessage);
|
||||
showSnackBar({barType: SNACK_BAR_TYPE.MESSAGE_COPIED, sourceScreen});
|
||||
}, [postMessage]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,23 +14,23 @@ import {dismissBottomSheet} from '@screens/navigation';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
location: typeof Screens[keyof typeof Screens];
|
||||
sourceScreen: typeof Screens[keyof typeof Screens];
|
||||
post: PostModel;
|
||||
teamId: string;
|
||||
}
|
||||
|
||||
const MarkAsUnreadOption = ({location, post, teamId}: Props) => {
|
||||
const MarkAsUnreadOption = ({sourceScreen, post, teamId}: Props) => {
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const onPress = useCallback(async () => {
|
||||
await dismissBottomSheet(Screens.POST_OPTIONS);
|
||||
if (location === Screens.THREAD) {
|
||||
if (sourceScreen === Screens.THREAD) {
|
||||
const threadId = post.rootId || post.id;
|
||||
markThreadAsUnread(serverUrl, teamId, threadId, post.id);
|
||||
} else {
|
||||
markPostAsUnread(serverUrl, post.id);
|
||||
}
|
||||
}, [location, post, serverUrl, teamId]);
|
||||
}, [sourceScreen, post, serverUrl, teamId]);
|
||||
|
||||
return (
|
||||
<BaseOption
|
||||
|
||||
@@ -31,25 +31,17 @@ type PostOptionsProps = {
|
||||
canReply: boolean;
|
||||
combinedPost?: Post;
|
||||
isSaved: boolean;
|
||||
location: typeof Screens[keyof typeof Screens];
|
||||
sourceScreen: typeof Screens[keyof typeof Screens];
|
||||
post: PostModel;
|
||||
thread?: ThreadModel;
|
||||
componentId: string;
|
||||
};
|
||||
|
||||
const PostOptions = ({
|
||||
canAddReaction,
|
||||
canDelete,
|
||||
canEdit,
|
||||
canMarkAsUnread,
|
||||
canPin,
|
||||
canReply,
|
||||
combinedPost,
|
||||
componentId,
|
||||
isSaved,
|
||||
location,
|
||||
post,
|
||||
thread,
|
||||
canAddReaction, canDelete, canEdit,
|
||||
canMarkAsUnread, canPin, canReply,
|
||||
combinedPost, componentId, isSaved,
|
||||
sourceScreen, post, thread,
|
||||
}: PostOptionsProps) => {
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
|
||||
@@ -75,7 +67,7 @@ const PostOptions = ({
|
||||
const canCopyPermalink = !isSystemPost && managedConfig?.copyAndPasteProtection !== 'true';
|
||||
const canCopyText = canCopyPermalink && post.message;
|
||||
|
||||
const shouldRenderFollow = !(location !== Screens.CHANNEL || !thread);
|
||||
const shouldRenderFollow = !(sourceScreen !== Screens.CHANNEL || !thread);
|
||||
|
||||
const snapPoints = [
|
||||
canAddReaction, canCopyPermalink, canCopyText,
|
||||
@@ -96,17 +88,26 @@ const PostOptions = ({
|
||||
{canMarkAsUnread && !isSystemPost &&
|
||||
<MarkAsUnreadOption
|
||||
post={post}
|
||||
location={location}
|
||||
sourceScreen={sourceScreen}
|
||||
/>
|
||||
}
|
||||
{canCopyPermalink &&
|
||||
<CopyPermalinkOption
|
||||
post={post}
|
||||
sourceScreen={sourceScreen}
|
||||
/>
|
||||
}
|
||||
{canCopyPermalink && <CopyPermalinkOption post={post}/>}
|
||||
{!isSystemPost &&
|
||||
<SaveOption
|
||||
isSaved={isSaved}
|
||||
postId={post.id}
|
||||
/>
|
||||
}
|
||||
{Boolean(canCopyText && post.message) && <CopyTextOption postMessage={post.message}/>}
|
||||
{Boolean(canCopyText && post.message) &&
|
||||
<CopyTextOption
|
||||
postMessage={post.message}
|
||||
sourceScreen={sourceScreen}
|
||||
/>}
|
||||
{canPin &&
|
||||
<PinChannelOption
|
||||
isPostPinned={post.isPinned}
|
||||
@@ -129,7 +130,7 @@ const PostOptions = ({
|
||||
};
|
||||
|
||||
// This fixes opening "post options modal" on top of "thread modal"
|
||||
const additionalSnapPoints = location === Screens.THREAD ? 3 : 2;
|
||||
const additionalSnapPoints = sourceScreen === Screens.THREAD ? 3 : 2;
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
|
||||
246
app/screens/snack_bar/index.tsx
Normal file
246
app/screens/snack_bar/index.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, Text, TouchableOpacity, useWindowDimensions, ViewStyle} from 'react-native';
|
||||
import {Gesture, GestureDetector, GestureHandlerRootView} from 'react-native-gesture-handler';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import Animated, {
|
||||
AnimatedStyleProp,
|
||||
Extrapolation,
|
||||
FadeIn,
|
||||
interpolate,
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import Toast, {TOAST_HEIGHT} from '@components/toast';
|
||||
import {Navigation as NavigationConstants, Screens} from '@constants';
|
||||
import {SNACK_BAR_CONFIG, SNACK_BAR_TYPE} from '@constants/snack_bar';
|
||||
import {TABLET_SIDEBAR_WIDTH} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {dismissOverlay} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const SNACK_BAR_WIDTH = 96;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
text: {
|
||||
color: theme.centerChannelBg,
|
||||
},
|
||||
undo: {
|
||||
color: theme.centerChannelBg,
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
gestureRoot: {
|
||||
flex: 1,
|
||||
height: 80,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
bottom: 104,
|
||||
},
|
||||
toast: {
|
||||
width: '100%',
|
||||
opacity: 1,
|
||||
backgroundColor: theme.centerChannelColor,
|
||||
},
|
||||
mobile: {
|
||||
backgroundColor: theme.centerChannelColor,
|
||||
width: `${SNACK_BAR_WIDTH}%`,
|
||||
opacity: 1,
|
||||
height: TOAST_HEIGHT,
|
||||
alignSelf: 'center' as const,
|
||||
borderRadius: 9,
|
||||
shadowColor: '#1F000000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 6,
|
||||
},
|
||||
shadowRadius: 4,
|
||||
shadowOpacity: 0.12,
|
||||
elevation: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type SnackBarProps = {
|
||||
componentId: string;
|
||||
onUndoPress?: () => void;
|
||||
barType: keyof typeof SNACK_BAR_TYPE;
|
||||
sourceScreen: typeof Screens[keyof typeof Screens];
|
||||
}
|
||||
|
||||
const SnackBar = ({barType, componentId, onUndoPress, sourceScreen}: SnackBarProps) => {
|
||||
const [showSnackBar, setShowSnackBar] = useState<boolean | undefined>();
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const isTablet = useIsTablet();
|
||||
const {width: windowWidth} = useWindowDimensions();
|
||||
const offset = useSharedValue(0);
|
||||
const isPanned = useSharedValue(false);
|
||||
const baseTimer = useRef<NodeJS.Timeout>();
|
||||
|
||||
const config = SNACK_BAR_CONFIG[barType];
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const onPressHandler = useCallback(() => {
|
||||
dismissOverlay(componentId);
|
||||
onUndoPress?.();
|
||||
}, [onUndoPress, componentId]);
|
||||
|
||||
const snackBarStyle = useMemo(() => {
|
||||
const diffWidth = windowWidth - TABLET_SIDEBAR_WIDTH;
|
||||
|
||||
let tabletStyle: Partial<ViewStyle>;
|
||||
|
||||
switch (true) {
|
||||
case sourceScreen === Screens.THREAD :
|
||||
tabletStyle = {
|
||||
marginLeft: 0,
|
||||
width: `${SNACK_BAR_WIDTH}%`,
|
||||
marginBottom: 30,
|
||||
};
|
||||
break;
|
||||
case sourceScreen === Screens.SAVED_POSTS :
|
||||
tabletStyle = {
|
||||
marginBottom: 20,
|
||||
marginLeft: TABLET_SIDEBAR_WIDTH,
|
||||
width: (SNACK_BAR_WIDTH / 100) * diffWidth,
|
||||
};
|
||||
break;
|
||||
case [Screens.PERMALINK, Screens.MENTIONS].includes(sourceScreen):
|
||||
tabletStyle = {
|
||||
marginBottom: 0,
|
||||
marginLeft: 0,
|
||||
width: (SNACK_BAR_WIDTH / 100) * windowWidth,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
tabletStyle = {
|
||||
marginBottom: 40,
|
||||
marginLeft: TABLET_SIDEBAR_WIDTH,
|
||||
width: (SNACK_BAR_WIDTH / 100) * diffWidth,
|
||||
};
|
||||
}
|
||||
|
||||
return [
|
||||
styles.mobile,
|
||||
isTablet && tabletStyle,
|
||||
] as AnimatedStyleProp<ViewStyle>;
|
||||
}, [theme, barType]);
|
||||
|
||||
const animatedMotion = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: interpolate(offset.value, [0, 100], [1, 0], Extrapolation.EXTEND),
|
||||
...(isPanned.value && {
|
||||
transform: [
|
||||
{translateY: offset.value},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}, [offset.value, isPanned.value]);
|
||||
|
||||
const hideSnackBar = () => {
|
||||
setShowSnackBar(false);
|
||||
};
|
||||
|
||||
const stopTimers = () => {
|
||||
if (baseTimer.current) {
|
||||
clearTimeout(baseTimer.current);
|
||||
}
|
||||
};
|
||||
|
||||
const gesture = Gesture.
|
||||
// eslint-disable-next-line new-cap
|
||||
Pan().
|
||||
activeOffsetY(20).
|
||||
onStart(() => {
|
||||
isPanned.value = true;
|
||||
runOnJS(stopTimers)();
|
||||
offset.value = withTiming(100, {duration: 200});
|
||||
}).
|
||||
onEnd(() => {
|
||||
runOnJS(hideSnackBar)();
|
||||
});
|
||||
|
||||
const animateHiding = (forceHiding: boolean) => {
|
||||
const duration = forceHiding ? 0 : 200;
|
||||
offset.value = withTiming(200, {duration}, () => runOnJS(hideSnackBar)());
|
||||
};
|
||||
|
||||
// This effect hides the snack bar after 3 seconds
|
||||
useEffect(() => {
|
||||
baseTimer.current = setTimeout(() => {
|
||||
if (!isPanned.value) {
|
||||
animateHiding(false);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
if (baseTimer.current) {
|
||||
clearTimeout(baseTimer.current);
|
||||
}
|
||||
};
|
||||
}, [isPanned.value]);
|
||||
|
||||
// This effect dismisses the Navigation Overlay after we have hidden the snack bar
|
||||
useEffect(() => {
|
||||
if (showSnackBar === false) {
|
||||
dismissOverlay(componentId);
|
||||
}
|
||||
}, [showSnackBar]);
|
||||
|
||||
// This effect checks if we are navigating away and if so, it dismisses the snack bar
|
||||
useEffect(() => {
|
||||
const onHideSnackBar = () => animateHiding(true);
|
||||
const screenWillAppear = Navigation.events().registerComponentWillAppearListener(onHideSnackBar);
|
||||
const screenDidDisappear = Navigation.events().registerComponentDidDisappearListener(onHideSnackBar);
|
||||
const tabPress = DeviceEventEmitter.addListener('tabPress', onHideSnackBar);
|
||||
const navigateToTab = DeviceEventEmitter.addListener(NavigationConstants.NAVIGATE_TO_TAB, onHideSnackBar);
|
||||
|
||||
return () => {
|
||||
screenWillAppear.remove();
|
||||
screenDidDisappear.remove();
|
||||
tabPress.remove();
|
||||
navigateToTab.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={styles.gestureRoot}>
|
||||
<GestureDetector gesture={gesture}>
|
||||
<Animated.View
|
||||
style={animatedMotion}
|
||||
entering={FadeIn.duration(300)}
|
||||
>
|
||||
<Toast
|
||||
animatedStyle={snackBarStyle}
|
||||
message={intl.formatMessage({id: config.id, defaultMessage: config.defaultMessage})}
|
||||
iconName={config.iconName}
|
||||
textStyle={styles.text}
|
||||
style={styles.toast}
|
||||
>
|
||||
{config.canUndo && onUndoPress && (
|
||||
<TouchableOpacity onPress={onPressHandler}>
|
||||
<Text style={styles.undo}>
|
||||
{intl.formatMessage({
|
||||
id: 'snack.bar.undo',
|
||||
defaultMessage: 'Undo',
|
||||
})}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</Toast>
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SnackBar;
|
||||
@@ -107,6 +107,7 @@ const ThreadOptions = ({
|
||||
<CopyPermalinkOption
|
||||
key='copy-link'
|
||||
post={post}
|
||||
sourceScreen={Screens.THREAD_OPTIONS}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
16
app/utils/snack_bar/index.ts
Normal file
16
app/utils/snack_bar/index.ts
Normal file
@@ -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;
|
||||
sourceScreen?: typeof Screens[keyof typeof Screens];
|
||||
};
|
||||
|
||||
export const showSnackBar = (passProps: ShowSnackBarArgs) => {
|
||||
const screen = Screens.SNACK_BAR;
|
||||
showOverlay(screen, passProps);
|
||||
};
|
||||
@@ -543,6 +543,11 @@
|
||||
"servers.login": "Log in",
|
||||
"servers.logout": "Log out",
|
||||
"servers.remove": "Remove",
|
||||
"snack.bar.follow.thread": "You're now following this thread",
|
||||
"snack.bar.link.copied": "Link copied to clipboard",
|
||||
"snack.bar.message.copied": "Text copied to clipboard",
|
||||
"snack.bar.mute.channel": "This channel was muted",
|
||||
"snack.bar.undo": "Undo",
|
||||
"status_dropdown.set_away": "Away",
|
||||
"status_dropdown.set_dnd": "Do Not Disturb",
|
||||
"status_dropdown.set_offline": "Offline",
|
||||
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -5,6 +5,7 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache 2.0",
|
||||
|
||||
Reference in New Issue
Block a user