mattermost-mobile/app/screens/snack_bar/index.tsx
Elias Nahum 2f3dfbbbfa
Update dependencies and upgrade to RN 0.76.5 (#8421)
* update dev deps

* partial update dependencies

* update watermelondb

* update rn to 0.76.5, expo to 52.0.18 and others

* upgrade android firebase

* upgrade detox deps

* fix package-lock.json

* update emm and paste-input

* update turbo-log

* update network library

* fix tests

* review feedback

* fix Keyboard blocking signIn button

* Fall back to iphone 14 iOS 17.2 simulator as app crashes on iOS 17.4

* changes in deleteCredentialsForServer is causing a crash

* withOptions x2 clearly is wrong

* re-add cli-platform-apple fix

* fix: RN 0.76.5 issue with bottom sheet disappearing (#8478)

* experiment, using view vs BottomSheetView

* revert previous & try disabling enableDynamicSizing

* revert an unintended removal

---------

Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com>
2025-01-16 07:11:32 -07:00

296 lines
9.8 KiB
TypeScript

// 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, type StyleProp, type ViewStyle} from 'react-native';
import {Gesture, GestureDetector, GestureHandlerRootView} from 'react-native-gesture-handler';
import {type ComponentEvent, Navigation} from 'react-native-navigation';
import Animated, {
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 {MESSAGE_TYPE, SNACK_BAR_CONFIG} 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';
import type {AvailableScreens} from '@typings/screens/navigation';
import type {ShowSnackBarArgs} from '@utils/snack_bar';
type SnackBarProps = {
componentId: AvailableScreens;
sourceScreen: AvailableScreens;
} & ShowSnackBarArgs;
const SNACK_BAR_WIDTH = 96;
const SNACK_BAR_HEIGHT = 56;
const SNACK_BAR_BOTTOM_RATIO = 0.04;
const caseScreens: AvailableScreens[] = [Screens.PERMALINK, Screens.MANAGE_CHANNEL_MEMBERS, Screens.MENTIONS, Screens.SAVED_MESSAGES];
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
text: {
color: theme.centerChannelBg,
},
undo: {
color: theme.centerChannelBg,
...typography('Body', 100, 'SemiBold'),
},
gestureRoot: {
flex: 1,
width: '100%',
position: 'absolute',
height: SNACK_BAR_HEIGHT,
},
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,
},
};
});
const SnackBar = ({
barType,
messageValues,
componentId,
onAction,
sourceScreen,
}: SnackBarProps) => {
const [showSnackBar, setShowSnackBar] = useState<boolean | undefined>();
const intl = useIntl();
const theme = useTheme();
const isTablet = useIsTablet();
const {width: windowWidth, height: windowHeight} = useWindowDimensions();
const offset = useSharedValue(0);
const isPanned = useSharedValue(false);
const baseTimer = useRef<NodeJS.Timeout>();
const mounted = useRef(false);
const userHasUndo = useRef(false);
const config = SNACK_BAR_CONFIG[barType];
const styles = getStyleSheet(theme);
const gestureRootStyle = useMemo(() => {
return {
bottom: SNACK_BAR_BOTTOM_RATIO * windowHeight,
};
}, [windowHeight]);
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}%`,
};
break;
case sourceScreen === Screens.CHANNEL_INFO:
tabletStyle = {
marginBottom: 40,
marginLeft: 0,
width: (SNACK_BAR_WIDTH / 100) * diffWidth,
};
break;
case caseScreens.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 StyleProp<ViewStyle>;
}, [windowWidth, styles.mobile, isTablet, sourceScreen]);
const toastStyle = useMemo(() => {
let backgroundColor: string;
switch (config?.type) {
case MESSAGE_TYPE.SUCCESS:
backgroundColor = theme.onlineIndicator;
break;
case MESSAGE_TYPE.ERROR:
backgroundColor = theme.errorTextColor;
break;
default:
backgroundColor = theme.centerChannelColor;
break;
}
return [styles.toast, {backgroundColor}];
}, [config?.type, styles.toast, theme.onlineIndicator, theme.errorTextColor, theme.centerChannelColor]);
const animatedMotion = useAnimatedStyle(() => {
return {
opacity: interpolate(offset.value, [0, 100], [1, 0], Extrapolation.EXTEND),
...(isPanned.value && {
transform: [
{translateY: offset.value},
],
}),
};
});
const hideSnackBar = () => {
if (mounted?.current) {
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 = useCallback((forceHiding: boolean) => {
const duration = forceHiding ? 0 : 200;
offset.value = withTiming(200, {duration}, () => runOnJS(hideSnackBar)());
}, [offset]);
const onUndoPressHandler = () => {
userHasUndo.current = true;
animateHiding(false);
};
// This effect hides the snack bar after 3 seconds
useEffect(() => {
mounted.current = true;
baseTimer.current = setTimeout(() => {
if (!isPanned.value) {
animateHiding(false);
}
}, 3000);
return () => {
stopTimers();
mounted.current = false;
};
}, [animateHiding, isPanned]);
// This effect dismisses the Navigation Overlay after we have hidden the snack bar
useEffect(() => {
if (showSnackBar === false) {
if (userHasUndo?.current) {
onAction?.();
}
dismissOverlay(componentId);
}
}, [showSnackBar, onAction, componentId]);
// This effect checks if we are navigating away and if so, it dismisses the snack bar
useEffect(() => {
const onHideSnackBar = (event?: ComponentEvent) => {
const evtComponentId = event?.componentId;
if ((componentId !== evtComponentId) && (sourceScreen !== evtComponentId)) {
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, gestureRootStyle]}
>
<GestureDetector gesture={gesture}>
<Animated.View
style={animatedMotion}
>
<Animated.View
entering={FadeIn.duration(300)}
>
<Toast
animatedStyle={snackBarStyle}
iconName={config.iconName}
message={intl.formatMessage(
{id: config.id, defaultMessage: config.defaultMessage},
messageValues,
)}
style={toastStyle}
textStyle={styles.text}
>
{config.canUndo && onAction && (
<TouchableOpacity onPress={onUndoPressHandler}>
<Text style={styles.undo}>
{intl.formatMessage({
id: 'snack.bar.undo',
defaultMessage: 'Undo',
})}
</Text>
</TouchableOpacity>
)}
</Toast>
</Animated.View>
</Animated.View>
</GestureDetector>
</GestureHandlerRootView>
);
};
export default SnackBar;