diff --git a/app/components/selected_users/index.tsx b/app/components/selected_users/index.tsx new file mode 100644 index 000000000..64472dab8 --- /dev/null +++ b/app/components/selected_users/index.tsx @@ -0,0 +1,265 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {LayoutChangeEvent, Platform, ScrollView, useWindowDimensions, View} from 'react-native'; +import Animated, {useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; + +import Toast from '@components/toast'; +import {General} from '@constants'; +import {useTheme} from '@context/theme'; +import {useIsTablet, useKeyboardHeightWithDuration} from '@hooks/device'; +import Button from '@screens/bottom_sheet/button'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; + +import SelectedUser, {USER_CHIP_BOTTOM_MARGIN, USER_CHIP_HEIGHT} from './selected_user'; + +type Props = { + + /** + * Name of the button Icon + */ + buttonIcon: string; + + /* + * Text displayed on the action button + */ + buttonText: string; + + /** + * the height of the parent container + */ + containerHeight?: number; + + /** + * the Y position of the first view in the parent container + */ + modalPosition?: number; + + /** + * A handler function that will select or deselect a user when clicked on. + */ + onPress: (selectedId?: {[id: string]: boolean}) => void; + + /** + * A handler function that will deselect a user when clicked on. + */ + onRemove: (id: string) => void; + + /** + * An object mapping user ids to a falsey value indicating whether or not they have been selected. + */ + selectedIds: {[id: string]: UserProfile}; + + /** + * callback to set the value of showToast + */ + setShowToast: (show: boolean) => void; + + /** + * show the toast + */ + showToast: boolean; + + /** + * How to display the names of users. + */ + teammateNameDisplay: string; + + /** + * toast Icon + */ + toastIcon?: string; + + /** + * toast Message + */ + toastMessage: string; +} + +const BUTTON_HEIGHT = 48; +const CHIP_HEIGHT_WITH_MARGIN = USER_CHIP_HEIGHT + USER_CHIP_BOTTOM_MARGIN; +const EXPOSED_CHIP_HEIGHT = 0.33 * USER_CHIP_HEIGHT; +const MAX_CHIP_ROWS = 2; +const SCROLL_PADDING_TOP = 20; +const PANEL_MAX_HEIGHT = SCROLL_PADDING_TOP + (CHIP_HEIGHT_WITH_MARGIN * MAX_CHIP_ROWS) + EXPOSED_CHIP_HEIGHT; +const TABLET_MARGIN_BOTTOM = 20; +const TOAST_BOTTOM_MARGIN = 24; + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + container: { + borderBottomWidth: 0, + borderColor: changeOpacity(theme.centerChannelColor, 0.16), + borderTopLeftRadius: 12, + borderTopRightRadius: 12, + borderWidth: 1, + maxHeight: PANEL_MAX_HEIGHT + BUTTON_HEIGHT, + overflow: 'hidden', + paddingHorizontal: 20, + shadowColor: theme.centerChannelColor, + shadowOffset: { + width: 0, + height: 8, + }, + shadowOpacity: 0.16, + shadowRadius: 24, + }, + toast: { + backgroundColor: theme.centerChannelColor, + }, + users: { + paddingTop: SCROLL_PADDING_TOP, + paddingBottom: 12, + flexDirection: 'row', + flexGrow: 1, + flexWrap: 'wrap', + }, + message: { + color: changeOpacity(theme.centerChannelColor, 0.6), + fontSize: 12, + marginRight: 5, + marginTop: 10, + marginBottom: 2, + }, + }; +}); + +export default function SelectedUsers({ + buttonIcon, buttonText, containerHeight = 0, + modalPosition = 0, onPress, onRemove, + selectedIds, setShowToast, showToast = false, + teammateNameDisplay, toastIcon, toastMessage, +}: Props) { + const theme = useTheme(); + const style = getStyleFromTheme(theme); + const isTablet = useIsTablet(); + const keyboard = useKeyboardHeightWithDuration(); + const insets = useSafeAreaInsets(); + const dimensions = useWindowDimensions(); + + const panelHeight = useSharedValue(0); + const [isVisible, setIsVisible] = useState(false); + const numberSelectedIds = Object.keys(selectedIds).length; + const bottomSpace = (dimensions.height - containerHeight - modalPosition); + + const users = useMemo(() => { + const u = []; + for (const id of Object.keys(selectedIds)) { + if (!selectedIds[id]) { + continue; + } + + u.push( + , + ); + } + return u; + }, [selectedIds, teammateNameDisplay, onRemove]); + + const totalPanelHeight = useDerivedValue(() => ( + isVisible ? panelHeight.value + BUTTON_HEIGHT : 0 + ), [isVisible, isTablet]); + + const marginBottom = useMemo(() => { + let margin = keyboard.height && Platform.OS === 'ios' ? keyboard.height - insets.bottom : 0; + if (isTablet) { + margin = keyboard.height ? (keyboard.height - bottomSpace - insets.bottom) : 0; + } + return margin; + }, [keyboard, isTablet, insets.bottom, bottomSpace]); + + const handlePress = useCallback(() => { + onPress(); + }, [onPress]); + + const onLayout = useCallback((e: LayoutChangeEvent) => { + panelHeight.value = Math.min(PANEL_MAX_HEIGHT, e.nativeEvent.layout.height); + }, []); + + const androidMaxHeight = Platform.select({ + android: { + maxHeight: isVisible ? undefined : 0, + }, + }); + + const animatedContainerStyle = useAnimatedStyle(() => ({ + marginBottom: withTiming(marginBottom, {duration: keyboard.duration}), + paddingBottom: (isVisible || Platform.OS === 'android') ? TABLET_MARGIN_BOTTOM + insets.bottom : 0, + backgroundColor: isVisible ? theme.centerChannelBg : 'transparent', + ...androidMaxHeight, + }), [marginBottom, keyboard.duration, isVisible, theme.centerChannelBg]); + + const animatedToastStyle = useAnimatedStyle(() => { + return { + bottom: TOAST_BOTTOM_MARGIN + totalPanelHeight.value, + opacity: withTiming(showToast ? 1 : 0, {duration: 250}), + position: 'absolute', + }; + }, [showToast, keyboard]); + + const animatedViewStyle = useAnimatedStyle(() => ({ + height: withTiming(totalPanelHeight.value, {duration: 250}), + borderWidth: isVisible ? 1 : 0, + maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT : 0, + }), [totalPanelHeight.value, isVisible]); + + const animatedButtonStyle = useAnimatedStyle(() => ({ + opacity: withTiming(isVisible ? 1 : 0, {duration: isVisible ? 500 : 100}), + }), [isVisible]); + + useEffect(() => { + setIsVisible(numberSelectedIds > 0); + }, [numberSelectedIds > 0]); + + // This effect hides the toast after 4 seconds + useEffect(() => { + let timer: NodeJS.Timeout; + if (showToast) { + timer = setTimeout(() => { + setShowToast(false); + }, 4000); + } + + return () => clearTimeout(timer); + }, [showToast]); + + return ( + + {showToast && + + } + + + + {users} + + + +