BottomSheet migration to react-native-bottom-sheet (#6907)

* BottomSheet migration to react-native-bottom-sheet

* Use correct scroll view for announcement bottom sheet

* ux review

* Fix post options bottom sheet snap point

* feedback review
This commit is contained in:
Elias Nahum 2023-01-05 09:51:51 +02:00 committed by GitHub
parent e9b8160f31
commit dbe565319d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 547 additions and 399 deletions

View file

@ -2962,28 +2962,6 @@ IN THE SOFTWARE.
"""
---
## reanimated-bottom-sheet
This product contains a modified version of 'reanimated-bottom-sheet' by Michał Osadnik.
Highly configurable component imitating native bottom sheet behavior, with fully native 60 FPS animations!
* HOMEPAGE:
* https://github.com/osdnk/react-native-reanimated-bottom-sheet
* LICENSE: MIT
Copyright 2019 present Michał Osadnik
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## semver

View file

@ -9,6 +9,7 @@ import {
View,
} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {dismissAnnouncement} from '@actions/local/systems';
import CompassIcon from '@components/compass_icon';
@ -17,6 +18,7 @@ import {ANNOUNCEMENT_BAR_HEIGHT} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {bottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -82,6 +84,7 @@ const AnnouncementBanner = ({
const intl = useIntl();
const serverUrl = useServerUrl();
const height = useSharedValue(0);
const {bottom} = useSafeAreaInsets();
const theme = useTheme();
const [visible, setVisible] = useState(false);
const style = getStyle(theme);
@ -100,19 +103,20 @@ const AnnouncementBanner = ({
defaultMessage: 'Announcement',
});
let snapPoint = SNAP_POINT_WITHOUT_DISMISS;
if (allowDismissal) {
snapPoint += DISMISS_BUTTON_HEIGHT;
}
const snapPoint = bottomSheetSnapPoint(
1,
SNAP_POINT_WITHOUT_DISMISS + (allowDismissal ? DISMISS_BUTTON_HEIGHT : 0),
bottom,
);
bottomSheet({
closeButtonId: CLOSE_BUTTON_ID,
title,
renderContent,
snapPoints: [snapPoint, 10],
snapPoints: [1, snapPoint],
theme,
});
}, [theme.sidebarHeaderTextColor, intl.locale, renderContent, allowDismissal]);
}, [theme.sidebarHeaderTextColor, intl.locale, renderContent, allowDismissal, bottom]);
const handleDismiss = useCallback(() => {
dismissAnnouncement(serverUrl, bannerText);

View file

@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BottomSheetScrollView} from '@gorhom/bottom-sheet';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import {ScrollView, Text, View} from 'react-native';
import Button from 'react-native-button';
import {ScrollView} from 'react-native-gesture-handler';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {dismissAnnouncement} from '@actions/local/systems';
@ -84,6 +84,8 @@ const ExpandedAnnouncementBanner = ({
return [style.container, {marginBottom: insets.bottom + 10}];
}, [style, insets.bottom]);
const Scroll = useMemo(() => (isTablet ? ScrollView : BottomSheetScrollView), [isTablet]);
return (
<View style={containerStyle}>
{!isTablet && (
@ -94,7 +96,7 @@ const ExpandedAnnouncementBanner = ({
})}
</Text>
)}
<ScrollView
<Scroll
style={style.scrollContainer}
>
<Markdown
@ -106,7 +108,7 @@ const ExpandedAnnouncementBanner = ({
theme={theme}
location={Screens.BOTTOM_SHEET}
/>
</ScrollView>
</Scroll>
<Button
containerStyle={buttonStyles.okay.button}
onPress={close}

View file

@ -24,12 +24,12 @@ type Props = {
testID?: string;
}
const OPTIONS_HEIGHT = 62;
export const CHANNEL_ACTIONS_OPTIONS_HEIGHT = 62;
const styles = StyleSheet.create({
wrapper: {
flexDirection: 'row',
height: OPTIONS_HEIGHT,
height: CHANNEL_ACTIONS_OPTIONS_HEIGHT,
},
separator: {
width: 8,

View file

@ -69,7 +69,7 @@ const AtMention = ({
const intl = useIntl();
const managedConfig = useManagedConfig<ManagedConfig>();
const theme = useTheme();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const serverUrl = useServerUrl();
const user = useMemo(() => {
@ -92,6 +92,7 @@ const AtMention = ({
// @ts-expect-error: The model constructor is hidden within WDB type definition
return new UserModel(database.get(USER), {username: ''});
}, [users, mentionName]);
const userMentionKeys = useMemo(() => {
if (mentionKeys) {
return mentionKeys;
@ -195,12 +196,12 @@ const AtMention = ({
bottomSheet({
closeButtonId: 'close-at-mention',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [1, bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}
}, [managedConfig, intl, theme, insets]);
}, [managedConfig, intl, theme, bottom]);
const mentionTextStyle = [];

View file

@ -71,7 +71,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
const intl = useIntl();
const managedConfig = useManagedConfig<ManagedConfig>();
const theme = useTheme();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const style = getStyleSheet(theme);
const SyntaxHighlighter = useMemo(() => {
if (!syntaxHighlighter) {
@ -147,12 +147,12 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
bottomSheet({
closeButtonId: 'close-code-block',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [1, bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}
}, [managedConfig, intl, insets, theme]);
}, [managedConfig, intl, bottom, theme]);
const trimContent = (text: string) => {
const lines = text.split('\n');

View file

@ -75,7 +75,7 @@ const MarkdownImage = ({
}: MarkdownImageProps) => {
const intl = useIntl();
const isTablet = useIsTablet();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const theme = useTheme();
const style = getStyleSheet(theme);
const managedConfig = useManagedConfig<ManagedConfig>();
@ -181,12 +181,12 @@ const MarkdownImage = ({
bottomSheet({
closeButtonId: 'close-mardown-image',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [1, bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}
}, [managedConfig, intl.locale, insets.bottom, theme]);
}, [managedConfig, intl.locale, bottom, theme]);
const handleOnError = useCallback(() => {
setFailed(true);

View file

@ -90,7 +90,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const LatexCodeBlock = ({content, theme}: Props) => {
const intl = useIntl();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const managedConfig = useManagedConfig<ManagedConfig>();
const styles = getStyleSheet(theme);
const languageDisplayName = getHighlightLanguageName('latex');
@ -161,12 +161,12 @@ const LatexCodeBlock = ({content, theme}: Props) => {
bottomSheet({
closeButtonId: 'close-code-block',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [1, bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}
}, [managedConfig?.copyAndPasteProtection, intl, insets, theme]);
}, [managedConfig?.copyAndPasteProtection, intl, bottom, theme]);
const onRenderErrorMessage = useCallback(({error}: {error: Error}) => {
return <Text style={styles.errorText}>{'Render error: ' + error.message}</Text>;

View file

@ -46,7 +46,7 @@ const parseLinkLiteral = (literal: string) => {
const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL}: MarkdownLinkProps) => {
const intl = useIntl();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const managedConfig = useManagedConfig<ManagedConfig>();
const serverUrl = useServerUrl();
const theme = useTheme();
@ -140,12 +140,12 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
bottomSheet({
closeButtonId: 'close-mardown-link',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [1, bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}
}, [managedConfig, intl, insets, theme]);
}, [managedConfig, intl, bottom, theme]);
const renderChildren = experimentalNormalizeMarkdownLinks ? parseChildren() : children;

View file

@ -69,11 +69,10 @@ export default function CameraQuickAction({
return;
}
const snap = bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom);
bottomSheet({
title: intl.formatMessage({id: 'mobile.camera_type.title', defaultMessage: 'Camera options'}),
renderContent,
snapPoints: [TITLE_HEIGHT + snap, 10],
snapPoints: [1, bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom) + TITLE_HEIGHT],
theme,
closeButtonId: 'camera-close-id',
});

View file

@ -4,13 +4,15 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
import PostPriorityPicker from '@components/post_priority/post_priority_picker';
import PostPriorityPicker, {COMPONENT_HEIGHT} from '@components/post_priority/post_priority_picker';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {ICON_SIZE} from '@constants/post_draft';
import {useTheme} from '@context/theme';
import {bottomSheet, dismissBottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {changeOpacity} from '@utils/theme';
type Props = {
@ -34,6 +36,7 @@ export default function PostPriorityAction({
}: Props) {
const intl = useIntl();
const theme = useTheme();
const {bottom} = useSafeAreaInsets();
const handlePostPriorityPicker = useCallback((postPriorityData: PostPriorityData) => {
updatePostPriority(postPriorityData);
@ -55,11 +58,11 @@ export default function PostPriorityAction({
bottomSheet({
title: intl.formatMessage({id: 'post_priority.picker.title', defaultMessage: 'Message priority'}),
renderContent,
snapPoints: [275, 10],
snapPoints: [1, bottomSheetSnapPoint(1, COMPONENT_HEIGHT, bottom)],
theme,
closeButtonId: 'post-priority-close-id',
});
}, [intl, renderContent, theme]);
}, [intl, renderContent, theme, bottom]);
const iconName = 'alert-circle-outline';
const iconColor = changeOpacity(theme.centerChannelColor, 0.64);

View file

@ -33,7 +33,7 @@ const styles = StyleSheet.create({
const Failed = ({post, theme}: FailedProps) => {
const intl = useIntl();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const serverUrl = useServerUrl();
const onPress = useCallback(() => {
@ -69,11 +69,11 @@ const Failed = ({post, theme}: FailedProps) => {
bottomSheet({
closeButtonId: 'close-post-failed',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [1, bottomSheetSnapPoint(2, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}, [insets]);
}, [bottom]);
return (
<TouchableOpacity

View file

@ -19,6 +19,8 @@ type Props = {
onSubmit: (data: PostPriorityData) => void;
};
export const COMPONENT_HEIGHT = 200;
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
backgroundColor: theme.centerChannelBg,

View file

@ -1,7 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
import React, {useCallback, useMemo} from 'react';
import {ListRenderItemInfo, StyleSheet, View} from 'react-native';
import {FlatList} from 'react-native-gesture-handler'; // Keep the FlatList from gesture handler so it works well with bottom sheet
@ -12,20 +13,21 @@ import TeamListItem from './team_list_item';
import type TeamModel from '@typings/database/models/servers/team';
type Props = {
teams: Array<Team|TeamModel>;
textColor?: string;
iconTextColor?: string;
iconBackgroundColor?: string;
onPress: (id: string) => void;
testID?: string;
selectedTeamId?: string;
onEndReached?: () => void;
iconTextColor?: string;
loading?: boolean;
onEndReached?: () => void;
onPress: (id: string) => void;
selectedTeamId?: string;
teams: Array<Team|TeamModel>;
testID?: string;
textColor?: string;
type?: BottomSheetList;
}
const styles = StyleSheet.create({
container: {
flexShrink: 1,
flexGrow: 1,
},
contentContainer: {
marginBottom: 4,
@ -34,7 +36,20 @@ const styles = StyleSheet.create({
const keyExtractor = (item: TeamModel) => item.id;
export default function TeamList({teams, textColor, iconTextColor, iconBackgroundColor, onPress, testID, selectedTeamId, onEndReached, loading = false}: Props) {
export default function TeamList({
iconBackgroundColor,
iconTextColor,
loading = false,
onEndReached,
onPress,
selectedTeamId,
teams,
testID,
textColor,
type = 'FlatList',
}: Props) {
const List = useMemo(() => (type === 'FlatList' ? FlatList : BottomSheetFlatList), [type]);
const renderTeam = useCallback(({item: t}: ListRenderItemInfo<Team|TeamModel>) => {
return (
<TeamListItem
@ -55,7 +70,7 @@ export default function TeamList({teams, textColor, iconTextColor, iconBackgroun
return (
<View style={styles.container}>
<FlatList
<List
data={teams}
renderItem={renderTeam}
keyExtractor={keyExtractor}

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BottomSheetProps} from '@gorhom/bottom-sheet';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, Text, TouchableOpacity, View, ViewStyle} from 'react-native';
@ -123,12 +124,17 @@ const UserAvatarsStack = ({breakAt = 3, channelId, location, style: baseContaine
/>
</>
);
const snap = bottomSheetSnapPoint(Math.min(users.length, 5), USER_ROW_HEIGHT, bottom);
const snapPoints: BottomSheetProps['snapPoints'] = [1, bottomSheetSnapPoint(Math.min(users.length, 5), USER_ROW_HEIGHT, bottom) + TITLE_HEIGHT];
if (users.length > 5) {
snapPoints.push('90%');
}
bottomSheet({
closeButtonId: 'close-set-user-status',
renderContent,
initialSnapIndex: 1,
snapPoints: ['90%', TITLE_HEIGHT + snap, 10],
snapPoints,
title: intl.formatMessage({id: 'mobile.participants.header', defaultMessage: 'Thread Participants'}),
theme,
});

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
import React, {useCallback, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, PanResponder, StyleProp, StyleSheet, TouchableOpacity, ViewStyle} from 'react-native';
@ -16,6 +17,7 @@ import type UserModel from '@typings/database/models/servers/user';
type Props = {
channelId: string;
location: string;
type?: BottomSheetList;
users: UserModel[];
};
@ -58,8 +60,8 @@ const Item = ({channelId, containerStyle, location, user}: ItemProps) => {
);
};
const UsersList = ({channelId, location, users}: Props) => {
const [enabled, setEnabled] = useState(false);
const UsersList = ({channelId, location, type = 'FlatList', users}: Props) => {
const [enabled, setEnabled] = useState(type === 'BottomSheetFlatList');
const [direction, setDirection] = useState<'down' | 'up'>('down');
const listRef = useRef<FlatList>(null);
const prevOffset = useRef(0);
@ -91,6 +93,16 @@ const UsersList = ({channelId, location, users}: Props) => {
/>
), [channelId, location]);
if (type === 'BottomSheetFlatList') {
return (
<BottomSheetFlatList
data={users}
renderItem={renderItem}
overScrollMode={'always'}
/>
);
}
return (
<FlatList
data={users}

View file

@ -12,6 +12,7 @@ export const DEFAULT_EMOJIS = [
export const LARGE_ICON_SIZE = 32;
export const LARGE_CONTAINER_SIZE = 48;
export const REACTION_PICKER_HEIGHT = 48;
export const REACTION_PICKER_MARGIN = 16;
export const SMALL_CONTAINER_SIZE = 44;
export const SMALL_ICON_BREAKPOINT = 410;
export const SMALL_ICON_SIZE = 28;
@ -21,6 +22,7 @@ export default {
LARGE_ICON_SIZE,
LARGE_CONTAINER_SIZE,
REACTION_PICKER_HEIGHT,
REACTION_PICKER_MARGIN,
SMALL_CONTAINER_SIZE,
SMALL_ICON_BREAKPOINT,
SMALL_ICON_SIZE,

View file

@ -12,7 +12,7 @@ export const TEAM_SIDEBAR_WIDTH = 72;
export const TABLET_HEADER_HEIGHT = 44;
export const TABLET_SIDEBAR_WIDTH = 320;
export const IOS_STATUS_BAR_HEIGHT = 20;
export const STATUS_BAR_HEIGHT = 20;
export const DEFAULT_HEADER_HEIGHT = Platform.select({android: 56, default: 44});
export const LARGE_HEADER_TITLE_HEIGHT = 60;
export const SUBTITLE_HEIGHT = 24;
@ -25,8 +25,6 @@ export const JOIN_CALL_BAR_HEIGHT = 38;
export const CURRENT_CALL_BAR_HEIGHT = 74;
export const CALL_ERROR_BAR_HEIGHT = 62;
export const QUICK_OPTIONS_HEIGHT = 270;
export const ANNOUNCEMENT_BAR_HEIGHT = 40;
export default {
@ -41,11 +39,10 @@ export default {
TABLET_SIDEBAR_WIDTH,
TEAM_SIDEBAR_WIDTH,
TABLET_HEADER_HEIGHT,
IOS_STATUS_BAR_HEIGHT,
STATUS_BAR_HEIGHT,
DEFAULT_HEADER_HEIGHT,
LARGE_HEADER_TITLE_HEIGHT,
SUBTITLE_HEIGHT,
KEYBOARD_TRACKING_OFFSET,
QUICK_OPTIONS_HEIGHT,
};

View file

@ -256,7 +256,7 @@ const CallScreen = ({
}: Props) => {
const intl = useIntl();
const theme = useTheme();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const {width, height} = useWindowDimensions();
const serverUrl = useServerUrl();
const {EnableRecordings} = useCallsConfig(serverUrl);
@ -407,11 +407,11 @@ const CallScreen = ({
await bottomSheet({
closeButtonId: 'close-other-actions',
renderContent,
snapPoints: [bottomSheetSnapPoint(items, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [1, bottomSheetSnapPoint(items, ITEM_HEIGHT, bottom)],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}, [insets, intl, theme, isHost, EnableRecordings, waitingForRecording, recording, startRecording,
}, [bottom, intl, theme, isHost, EnableRecordings, waitingForRecording, recording, startRecording,
recordOptionTitle, stopRecording, stopRecordingOptionTitle, style, switchToThread, callThreadOptionTitle,
openChannelOptionTitle]);

View file

@ -2,13 +2,14 @@
// See LICENSE.txt for license information.
import React from 'react';
import {GestureResponderEvent, StyleSheet, Text, View} from 'react-native';
import {GestureResponderEvent, Platform, Text, useWindowDimensions, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity} from '@utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
disabled?: boolean;
@ -18,22 +19,43 @@ type Props = {
text?: string;
}
const styles = StyleSheet.create({
button: {
display: 'flex',
flexDirection: 'row',
},
icon_container: {
width: 24,
height: 24,
top: -1,
marginRight: 4,
},
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
button: {
display: 'flex',
flexDirection: 'row',
},
buttonContainer: {
paddingHorizontal: 20,
},
container: {
backgroundColor: theme.centerChannelBg,
},
iconContainer: {
width: 24,
height: 24,
top: -1,
marginRight: 4,
},
separator: {
height: 1,
right: 20,
borderTopWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
marginBottom: 20,
},
};
});
export default function BottomSheetButton({disabled = false, onPress, icon, testID, text}: Props) {
const theme = useTheme();
export const BUTTON_HEIGHT = 101;
function BottomSheetButton({disabled = false, onPress, icon, testID, text}: Props) {
const theme = useTheme();
const dimensions = useWindowDimensions();
const isTablet = useIsTablet();
const styles = getStyleSheet(theme);
const separatorWidth = Math.max(dimensions.width, 450);
const buttonType = disabled ? 'disabled' : 'default';
const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType);
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType);
@ -41,27 +63,35 @@ export default function BottomSheetButton({disabled = false, onPress, icon, test
const iconColor = disabled ? changeOpacity(theme.centerChannelColor, 0.32) : theme.buttonColor;
return (
<TouchableWithFeedback
onPress={onPress}
type='opacity'
style={[styles.button, styleButtonBackground]}
testID={testID}
>
{icon && (
<View style={styles.icon_container}>
<CompassIcon
size={24}
name={icon}
color={iconColor}
/>
</View>
)}
{text && (
<Text
style={styleButtonText}
>{text}</Text>
)}
<View style={styles.container}>
<View style={[styles.separator, {width: separatorWidth}]}/>
<View style={styles.buttonContainer}>
<TouchableWithFeedback
onPress={onPress}
type='opacity'
style={[styles.button, styleButtonBackground]}
testID={testID}
>
{icon && (
<View style={styles.iconContainer}>
<CompassIcon
size={24}
name={icon}
color={iconColor}
/>
</View>
)}
{text && (
<Text
style={styleButtonText}
>{text}</Text>
)}
</TouchableWithFeedback>
</TouchableWithFeedback>
<View style={{paddingBottom: Platform.select({ios: (isTablet ? 20 : 32), android: 20})}}/>
</View>
</View>
);
}
export default BottomSheetButton;

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {GestureResponderEvent, Platform, Text, useWindowDimensions, View} from 'react-native';
import {GestureResponderEvent, Text, useWindowDimensions, View} from 'react-native';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
@ -33,7 +33,7 @@ export const TITLE_SEPARATOR_MARGIN_TABLET = 20;
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {
flex: 1,
flexGrow: 1,
},
titleContainer: {
marginTop: TITLE_MARGIN_TOP,
@ -83,18 +83,15 @@ const BottomSheetContent = ({buttonText, buttonIcon, children, disableButton, on
{children}
</>
{showButton && (
<>
<View style={[styles.separator, {width: separatorWidth}]}/>
<Button
disabled={disableButton}
onPress={onPress}
icon={buttonIcon}
testID={buttonTestId}
text={buttonText}
/>
<View style={{paddingBottom: Platform.select({ios: (isTablet ? 20 : 32), android: 20})}}/>
</>
)}
<Button
disabled={disableButton}
onPress={onPress}
icon={buttonIcon}
testID={buttonTestId}
text={buttonText}
/>
)
}
</View>
);
};

View file

@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactNode, useCallback, useEffect, useRef} from 'react';
import {DeviceEventEmitter, Keyboard, StyleSheet, View} from 'react-native';
import {State, TapGestureHandler} from 'react-native-gesture-handler';
import {Navigation as RNN} from 'react-native-navigation';
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import RNBottomSheet from 'reanimated-bottom-sheet';
import BottomSheetM, {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetFooterProps} from '@gorhom/bottom-sheet';
import React, {ReactNode, useCallback, useEffect, useMemo, useRef} from 'react';
import {DeviceEventEmitter, Keyboard, View} from 'react-native';
import useNavButtonPressed from '@app/hooks/navigation_button_pressed';
import {Events} from '@constants';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@ -18,36 +16,94 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import Indicator from './indicator';
type SlideUpPanelProps = {
import type {WithSpringConfig} from 'react-native-reanimated';
export {default as BottomSheetButton, BUTTON_HEIGHT} from './button';
export {default as BottomSheetContent, TITLE_HEIGHT} from './content';
type Props = {
closeButtonId?: string;
componentId: string;
initialSnapIndex?: number;
footerComponent?: React.FC<BottomSheetFooterProps>;
renderContent: () => ReactNode;
snapPoints?: Array<string | number>;
testID?: string;
}
export const PADDING_TOP_MOBILE = 20;
export const PADDING_TOP_TABLET = 8;
const PADDING_TOP_MOBILE = 20;
const PADDING_TOP_TABLET = 8;
const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderContent, snapPoints = ['90%', '50%', 50], testID}: SlideUpPanelProps) => {
const sheetRef = useRef<RNBottomSheet>(null);
export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
bottomSheet: {
borderTopStartRadius: 24,
borderTopEndRadius: 24,
shadowOffset: {
width: 0,
height: 8,
},
shadowOpacity: 0.12,
shadowRadius: 24,
shadowColor: '#000',
elevation: 24,
},
bottomSheetBackground: {
backgroundColor: theme.centerChannelBg,
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
},
content: {
flex: 1,
paddingHorizontal: 20,
paddingTop: PADDING_TOP_MOBILE,
},
contentTablet: {
paddingTop: PADDING_TOP_TABLET,
},
separator: {
height: 1,
borderTopWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
},
};
});
export const animatedConfig: Omit<WithSpringConfig, 'velocity'> = {
damping: 50,
mass: 0.3,
stiffness: 121.6,
overshootClamping: true,
restSpeedThreshold: 0.3,
restDisplacementThreshold: 0.3,
};
const BottomSheet = ({
closeButtonId,
componentId,
initialSnapIndex = 1,
footerComponent,
renderContent,
snapPoints = [1, '50%', '90%'],
testID,
}: Props) => {
const sheetRef = useRef<BottomSheetM>(null);
const isTablet = useIsTablet();
const theme = useTheme();
const firstRun = useRef(isTablet);
const lastSnap = snapPoints.length - 1;
const backdropOpacity = useSharedValue(0);
const styles = getStyleSheet(theme);
const bottomSheetBackgroundStyle = useMemo(() => [
styles.bottomSheetBackground,
{borderWidth: isTablet ? 0 : 1},
], [isTablet, styles]);
const close = useCallback(() => {
if (firstRun.current) {
dismissModal({componentId});
}
dismissModal({componentId});
}, [componentId]);
useEffect(() => {
const listener = DeviceEventEmitter.addListener(Events.CLOSE_BOTTOM_SHEET, () => {
if (sheetRef.current) {
sheetRef.current.snapTo(lastSnap);
sheetRef.current.close();
} else {
close();
}
@ -58,84 +114,40 @@ const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderCo
const handleClose = useCallback(() => {
if (sheetRef.current) {
sheetRef.current.snapTo(1);
sheetRef.current.close();
} else {
close();
}
}, []);
const handleCloseEnd = useCallback(() => {
if (firstRun.current) {
backdropOpacity.value = 0;
setTimeout(close, 250);
const handleDismissIfNeeded = useCallback((index: number) => {
if (index <= 0) {
close();
}
}, []);
const handleOpenStart = useCallback(() => {
backdropOpacity.value = 1;
}, []);
useAndroidHardwareBackHandler(componentId, handleClose);
useNavButtonPressed(closeButtonId || '', componentId, close, [close]);
useEffect(() => {
hapticFeedback();
Keyboard.dismiss();
sheetRef.current?.snapTo(initialSnapIndex);
backdropOpacity.value = 1;
const t = setTimeout(() => {
firstRun.current = true;
}, 100);
return () => clearTimeout(t);
}, []);
useEffect(() => {
const navigationEvents = RNN.events().registerNavigationButtonPressedListener(({buttonId}) => {
if (closeButtonId && buttonId === closeButtonId) {
close();
}
});
return () => navigationEvents.remove();
}, [close]);
const backdropStyle = useAnimatedStyle(() => ({
opacity: withTiming(backdropOpacity.value, {duration: 250, easing: Easing.inOut(Easing.linear)}),
backgroundColor: 'rgba(0, 0, 0, 0.6)',
}));
const renderBackdrop = () => {
const renderBackdrop = useCallback((props: BottomSheetBackdropProps) => {
return (
<TapGestureHandler
shouldCancelWhenOutside={true}
maxDist={10}
onHandlerStateChange={(event) => {
if (event.nativeEvent.state === State.END && event.nativeEvent.oldState === State.ACTIVE) {
sheetRef.current?.snapTo(lastSnap);
}
}}
testID={`${testID}.backdrop`}
>
<Animated.View
style={[StyleSheet.absoluteFill, backdropStyle]}
/>
</TapGestureHandler>
<BottomSheetBackdrop
{...props}
disappearsOnIndex={0}
appearsOnIndex={1}
opacity={0.6}
/>
);
};
}, []);
const renderContainerContent = () => (
<View
style={{
backgroundColor: theme.centerChannelBg,
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
borderWidth: isTablet ? 0 : 1,
opacity: 1,
paddingHorizontal: 20,
paddingTop: isTablet ? PADDING_TOP_TABLET : PADDING_TOP_MOBILE,
height: '100%',
width: '100%',
alignSelf: 'center',
}}
style={[styles.content, isTablet && styles.contentTablet]}
testID={`${testID}.screen`}
>
{renderContent()}
@ -143,7 +155,6 @@ const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderCo
);
if (isTablet) {
const styles = getStyleSheet(theme);
return (
<>
<View style={styles.separator}/>
@ -153,32 +164,22 @@ const BottomSheet = ({closeButtonId, componentId, initialSnapIndex = 0, renderCo
}
return (
<>
<RNBottomSheet
ref={sheetRef}
snapPoints={snapPoints}
borderRadius={12}
initialSnap={snapPoints.length - 1}
renderContent={renderContainerContent}
onCloseEnd={handleCloseEnd}
onOpenStart={handleOpenStart}
enabledBottomInitialAnimation={false}
renderHeader={Indicator}
enabledContentTapInteraction={false}
/>
{renderBackdrop()}
</>
<BottomSheetM
ref={sheetRef}
index={initialSnapIndex}
snapPoints={snapPoints}
animateOnMount={true}
backdropComponent={renderBackdrop}
onChange={handleDismissIfNeeded}
animationConfigs={animatedConfig}
handleComponent={Indicator}
style={styles.bottomSheet}
backgroundStyle={bottomSheetBackgroundStyle}
footerComponent={footerComponent}
>
{renderContainerContent()}
</BottomSheetM>
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
separator: {
height: 1,
borderTopWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
},
};
});
export default BottomSheet;

View file

@ -6,9 +6,9 @@ import {Animated, StyleSheet, View} from 'react-native';
const styles = StyleSheet.create({
dragIndicatorContainer: {
marginVertical: 10,
alignItems: 'center',
justifyContent: 'center',
top: -15,
},
dragIndicator: {
backgroundColor: 'white',

View file

@ -48,7 +48,7 @@ export default function ChannelDropdown({
sharedChannelsEnabled,
}: Props) {
const intl = useIntl();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const theme = useTheme();
const style = getStyleFromTheme(theme);
@ -73,11 +73,11 @@ export default function ChannelDropdown({
items += 1;
}
const itemsSnap = bottomSheetSnapPoint(items, ITEM_HEIGHT, insets.bottom) + TITLE_HEIGHT;
const itemsSnap = bottomSheetSnapPoint(items, ITEM_HEIGHT, bottom) + TITLE_HEIGHT;
bottomSheet({
title: intl.formatMessage({id: 'browse_channels.dropdownTitle', defaultMessage: 'Show'}),
renderContent,
snapPoints: [itemsSnap, 10],
snapPoints: [1, itemsSnap],
closeButtonId: 'close',
theme,
});

View file

@ -4,14 +4,16 @@
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, Text, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {bottomSheetSnapPoint} from '@app/utils/helpers';
import {CHANNEL_ACTIONS_OPTIONS_HEIGHT} from '@components/channel_actions/channel_actions';
import CompassIcon from '@components/compass_icon';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import NavigationHeader from '@components/navigation_header';
import {ITEM_HEIGHT} from '@components/option_item';
import RoundedHeaderContext from '@components/rounded_header_context';
import {General, Screens} from '@constants';
import {QUICK_OPTIONS_HEIGHT} from '@constants/view';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {useDefaultHeaderHeight} from '@hooks/header';
@ -22,7 +24,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import OtherMentionsBadge from './other_mentions_badge';
import QuickActions from './quick_actions';
import QuickActions, {MARGIN, SEPARATOR_HEIGHT} from './quick_actions';
import type {HeaderRightButton} from '@components/navigation_header/header';
@ -70,6 +72,7 @@ const ChannelHeader = ({
}: ChannelProps) => {
const intl = useIntl();
const isTablet = useIsTablet();
const {bottom} = useSafeAreaInsets();
const theme = useTheme();
const styles = getStyleSheet(theme);
const defaultHeight = useDefaultHeaderHeight();
@ -132,7 +135,8 @@ const ChannelHeader = ({
}
// When calls is enabled, we need space to move the "Copy Link" from a button to an option
const height = QUICK_OPTIONS_HEIGHT + (callsAvailable && !isDMorGM ? ITEM_HEIGHT : 0);
const items = callsAvailable && !isDMorGM ? 3 : 2;
const height = CHANNEL_ACTIONS_OPTIONS_HEIGHT + SEPARATOR_HEIGHT + MARGIN + (items * ITEM_HEIGHT);
const renderContent = () => {
return (
@ -147,11 +151,11 @@ const ChannelHeader = ({
bottomSheet({
title: '',
renderContent,
snapPoints: [height, 10],
snapPoints: [1, bottomSheetSnapPoint(1, height, bottom)],
theme,
closeButtonId: 'close-channel-quick-actions',
});
}, [channelId, isDMorGM, isTablet, onTitlePress, theme, callsAvailable]);
}, [bottom, channelId, isDMorGM, isTablet, onTitlePress, theme, callsAvailable]);
const rightButtons: HeaderRightButton[] = useMemo(() => ([

View file

@ -8,7 +8,6 @@ import ChannelActions from '@components/channel_actions';
import CopyChannelLinkOption from '@components/channel_actions/copy_channel_link_option';
import InfoBox from '@components/channel_actions/info_box';
import LeaveChannelLabel from '@components/channel_actions/leave_channel_label';
import {QUICK_OPTIONS_HEIGHT} from '@constants/view';
import {useTheme} from '@context/theme';
import {dismissBottomSheet} from '@screens/navigation';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -19,20 +18,23 @@ type Props = {
isDMorGM: boolean;
}
export const SEPARATOR_HEIGHT = 17;
export const MARGIN = 8;
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
minHeight: QUICK_OPTIONS_HEIGHT,
flex: 1,
},
line: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
height: 1,
marginVertical: 8,
marginVertical: MARGIN,
},
wrapper: {
marginBottom: 8,
marginBottom: MARGIN,
},
separator: {
width: 8,
width: MARGIN,
},
}));

View file

@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import NetworkManager from '@managers/network_manager';
import {TITLE_HEIGHT} from '@screens/bottom_sheet/content';
import PanelItem from '@screens/edit_profile/components/panel_item';
import {bottomSheet} from '@screens/navigation';
import PickerUtil from '@utils/file/file_picker';
@ -121,13 +122,12 @@ const ProfileImagePicker = ({
);
};
const snapPointsCount = canRemovePicture ? 5 : 4;
const snapPoint = bottomSheetSnapPoint(snapPointsCount, ITEM_HEIGHT, bottom);
const snapPoint = bottomSheetSnapPoint(4, ITEM_HEIGHT, bottom) + TITLE_HEIGHT;
return bottomSheet({
closeButtonId: 'close-edit-profile',
renderContent,
snapPoints: [snapPoint, 10],
snapPoints: [1, snapPoint],
title: 'Change profile photo',
theme,
});

View file

@ -129,7 +129,7 @@ const UserStatus = ({currentUser}: Props) => {
bottomSheet({
closeButtonId: 'close-set-user-status',
renderContent,
snapPoints: [(snapPoint + TITLE_HEIGHT), 10],
snapPoints: [1, (snapPoint + TITLE_HEIGHT)],
title: intl.formatMessage({id: 'user_status.title', defaultMessage: 'Status'}),
theme,
});

View file

@ -105,7 +105,7 @@ const ChannelListHeader = ({
const theme = useTheme();
const isTablet = useIsTablet();
const intl = useIntl();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const serverDisplayName = useServerDisplayName();
const marginLeft = useSharedValue(iconPad ? 50 : 0);
const styles = getStyles(theme);
@ -150,11 +150,11 @@ const ChannelListHeader = ({
bottomSheet({
closeButtonId,
renderContent,
snapPoints: [bottomSheetSnapPoint(items, ITEM_HEIGHT, insets.bottom) + (separators * SEPARATOR_HEIGHT), 10],
snapPoints: [1, bottomSheetSnapPoint(items, ITEM_HEIGHT, bottom) + (separators * SEPARATOR_HEIGHT)],
theme,
title: intl.formatMessage({id: 'home.header.plus_menu', defaultMessage: 'Options'}),
});
}, [intl, insets, isTablet, theme]);
}, [intl, bottom, isTablet, theme]);
const onPushAlertPress = useCallback(() => {
if (pushProxyStatus === PUSH_PROXY_STATUS_NOT_AVAILABLE) {

View file

@ -1,9 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BottomSheetProps} from '@gorhom/bottom-sheet';
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {StyleSheet} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import ServerIcon from '@components/server_icon';
import {useServerUrl} from '@context/server';
@ -11,14 +13,21 @@ import {useTheme} from '@context/theme';
import {subscribeAllServers} from '@database/subscription/servers';
import {subscribeUnreadAndMentionsByServer, UnreadObserverArgs} from '@database/subscription/unreads';
import {useIsTablet} from '@hooks/device';
import {BUTTON_HEIGHT, TITLE_HEIGHT} from '@screens/bottom_sheet';
import {bottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {sortServersByDisplayName} from '@utils/server';
import ServerList from './servers_list';
import ServerList, {AddServerButton} from './servers_list';
import type ServersModel from '@typings/database/models/app/servers';
import type {UnreadMessages, UnreadSubscription} from '@typings/database/subscriptions';
export type ServersRef = {
openServers: () => void;
}
export const SERVER_ITEM_HEIGHT = 72;
const subscriptions: Map<string, UnreadSubscription> = new Map();
const styles = StyleSheet.create({
@ -34,16 +43,13 @@ const styles = StyleSheet.create({
},
});
export type ServersRef = {
openServers: () => void;
}
const Servers = React.forwardRef<ServersRef>((props, ref) => {
const Servers = React.forwardRef<ServersRef>((_, ref) => {
const intl = useIntl();
const [total, setTotal] = useState<UnreadMessages>({mentions: 0, unread: false});
const registeredServers = useRef<ServersModel[]|undefined>();
const currentServerUrl = useServerUrl();
const isTablet = useIsTablet();
const {bottom} = useSafeAreaInsets();
const theme = useTheme();
const updateTotal = () => {
@ -111,21 +117,25 @@ const Servers = React.forwardRef<ServersRef>((props, ref) => {
);
};
const snapPoints = ['50%', 10];
if (registeredServers.current.length > 3) {
snapPoints[0] = '90%';
const snapPoints: BottomSheetProps['snapPoints'] = [
1,
bottomSheetSnapPoint(Math.min(2.5, registeredServers.current.length), 72, bottom) + TITLE_HEIGHT + BUTTON_HEIGHT,
];
if (registeredServers.current.length > 1) {
snapPoints.push('90%');
}
const closeButtonId = 'close-your-servers';
bottomSheet({
closeButtonId,
renderContent,
footerComponent: AddServerButton,
snapPoints,
theme,
title: intl.formatMessage({id: 'your.servers', defaultMessage: 'Your servers'}),
});
}
}, [isTablet, theme]);
}, [bottom, isTablet, theme]);
useImperativeHandle(ref, () => ({
openServers: onPress,

View file

@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BottomSheetFooter, BottomSheetFooterProps} from '@gorhom/bottom-sheet';
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {useTheme} from '@context/theme';
import {BottomSheetButton} from '@screens/bottom_sheet';
import {addNewServer} from '@utils/server';
const AddServerButton = (props: BottomSheetFooterProps) => {
const theme = useTheme();
const {formatMessage} = useIntl();
const onAddServer = useCallback(async () => {
addNewServer(theme);
}, []);
return (
<BottomSheetFooter {...props}>
<BottomSheetButton
onPress={onAddServer}
icon='plus'
testID='servers.create_button'
text={formatMessage({id: 'servers.create_button', defaultMessage: 'Add a server'})}
/>
</BottomSheetFooter>
);
};
export default AddServerButton;

View file

@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {ListRenderItemInfo, StyleSheet, View} from 'react-native';
import {FlatList} from 'react-native-gesture-handler';
import {FlatList, ListRenderItemInfo, StyleSheet, View} from 'react-native';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
@ -16,13 +16,15 @@ import ServerItem from './server_item';
import type ServersModel from '@typings/database/models/app/servers';
export {default as AddServerButton} from './add_server_button';
type Props = {
servers: ServersModel[];
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexGrow: 1,
},
contentContainer: {
marginVertical: 4,
@ -51,18 +53,20 @@ const ServerList = ({servers}: Props) => {
);
}, []);
const List = useMemo(() => (isTablet ? FlatList : BottomSheetFlatList), [isTablet]);
return (
<BottomSheetContent
buttonIcon='plus'
buttonText={intl.formatMessage({id: 'servers.create_button', defaultMessage: 'Add a server'})}
onPress={onAddServer}
showButton={true}
showButton={isTablet}
showTitle={!isTablet}
testID='server_list'
title={intl.formatMessage({id: 'your.servers', defaultMessage: 'Your servers'})}
>
<View style={[styles.container, {marginTop: isTablet ? 12 : 0}]}>
<FlatList
<List
data={servers}
renderItem={renderServer}
keyExtractor={keyExtractor}

View file

@ -17,7 +17,7 @@ type Props = {
title: string;
}
export default function SelectTeamSlideUp({teams, title, setTeamId, teamId}: Props) {
export default function BottomSheetTeamList({teams, title, setTeamId, teamId}: Props) {
const isTablet = useIsTablet();
const showTitle = !isTablet && Boolean(teams.length);
@ -38,6 +38,7 @@ export default function SelectTeamSlideUp({teams, title, setTeamId, teamId}: Pro
teams={teams}
onPress={onPress}
testID='search.select_team_slide_up.team_list'
type={isTablet ? 'FlatList' : 'BottomSheetFlatList'}
/>
</BottomSheetContent>
);

View file

@ -40,7 +40,8 @@ export const showMobileOptionsBottomSheet = ({
closeButtonId: 'close-search-file-options',
renderContent,
snapPoints: [
bottomSheetSnapPoint(numOptions, ITEM_HEIGHT, insets.bottom) + HEADER_HEIGHT, 10,
1,
bottomSheetSnapPoint(numOptions, ITEM_HEIGHT, insets.bottom) + HEADER_HEIGHT,
],
theme,
title: '',

View file

@ -93,12 +93,13 @@ const Header = ({
const snapPoints = useMemo(() => {
return [
1,
bottomSheetSnapPoint(
NUMBER_FILTER_ITEMS,
FILTER_ITEM_HEIGHT,
bottom,
) + TITLE_HEIGHT + DIVIDERS_HEIGHT + (isTablet ? TITLE_SEPARATOR_MARGIN_TABLET : TITLE_SEPARATOR_MARGIN),
10];
];
}, []);
const handleFilterPress = useCallback(() => {

View file

@ -3,23 +3,28 @@
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {View, useWindowDimensions} from 'react-native';
import {View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import CompassIcon from '@components/compass_icon';
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import TeamIcon from '@components/team_sidebar/team_list/team_item/team_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useTheme} from '@context/theme';
import {TITLE_HEIGHT} from '@screens/bottom_sheet';
import {bottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {preventDoubleTap} from '@utils/tap';
import {getTeamsSnapHeight} from '@utils/team_list';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import SelectTeamSlideUp from './select_team_slideup';
import BottomSheetTeamList from './bottom_sheet_team_list';
import type {BottomSheetProps} from '@gorhom/bottom-sheet';
import type TeamModel from '@typings/database/models/servers/team';
const MENU_DOWN_ICON_SIZE = 24;
const NO_TEAMS_HEIGHT = 392;
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
teamContainer: {
@ -52,9 +57,8 @@ type Props = {
const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}: Props) => {
const intl = useIntl();
const theme = useTheme();
const dimensions = useWindowDimensions();
const styles = getStyleFromTheme(theme);
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const selectedTeam = teams.find((t) => t.id === teamId);
@ -63,7 +67,7 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}:
const handleTeamChange = useCallback(preventDoubleTap(() => {
const renderContent = () => {
return (
<SelectTeamSlideUp
<BottomSheetTeamList
setTeamId={setTeamId}
teams={teams}
teamId={teamId}
@ -72,15 +76,23 @@ const TeamPickerIcon = ({size = 24, divider = false, setTeamId, teams, teamId}:
);
};
const height = getTeamsSnapHeight({dimensions, teams, insets});
const snapPoints: BottomSheetProps['snapPoints'] = [
1,
teams.length ? (bottomSheetSnapPoint(Math.min(3, teams.length), ITEM_HEIGHT, bottom) + TITLE_HEIGHT) : NO_TEAMS_HEIGHT,
];
if (teams.length > 3) {
snapPoints.push('90%');
}
bottomSheet({
closeButtonId: 'close-team_list',
renderContent,
snapPoints: [height, 10],
snapPoints,
theme,
title,
});
}), [theme, setTeamId, teamId, teams]);
}), [theme, setTeamId, teamId, teams, bottom]);
return (
<>

View file

@ -17,6 +17,7 @@ import NavigationStore from '@store/navigation_store';
import {appearanceControlledScreens, mergeNavigationOptions} from '@utils/navigation';
import {changeOpacity, setNavigatorStyles} from '@utils/theme';
import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet';
import type {LaunchProps} from '@typings/launch';
import type {NavButtons} from '@typings/screens/navigation';
@ -738,13 +739,14 @@ export async function dismissOverlay(componentId: string) {
type BottomSheetArgs = {
closeButtonId: string;
initialSnapIndex?: number;
footerComponent?: React.FC<BottomSheetFooterProps>;
renderContent: () => JSX.Element;
snapPoints: Array<number | string>;
theme: Theme;
title: string;
}
export async function bottomSheet({title, renderContent, snapPoints, initialSnapIndex = 0, theme, closeButtonId}: BottomSheetArgs) {
export async function bottomSheet({title, renderContent, footerComponent, snapPoints, initialSnapIndex = 1, theme, closeButtonId}: BottomSheetArgs) {
const {isSplitView} = await isRunningInSplitView();
const isTablet = Device.IS_TABLET && !isSplitView;
@ -753,12 +755,14 @@ export async function bottomSheet({title, renderContent, snapPoints, initialSnap
closeButtonId,
initialSnapIndex,
renderContent,
footerComponent,
snapPoints,
}, bottomSheetModalOptions(theme, closeButtonId));
} else {
showModalOverCurrentContext(Screens.BOTTOM_SHEET, {
initialSnapIndex,
renderContent,
footerComponent,
snapPoints,
}, bottomSheetModalOptions(theme));
}

View file

@ -1,15 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BottomSheetProps, BottomSheetScrollView} from '@gorhom/bottom-sheet';
import {useManagedConfig} from '@mattermost/react-native-emm';
import React, {useMemo} from 'react';
import {ScrollView} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {CopyPermalinkOption, FollowThreadOption, ReplyOption, SaveOption} from '@components/common_post_options';
import {ITEM_HEIGHT} from '@components/option_item';
import {Screens} from '@constants';
import {REACTION_PICKER_HEIGHT, REACTION_PICKER_MARGIN} from '@constants/reaction_picker';
import {useIsTablet} from '@hooks/device';
import useNavButtonPressed from '@hooks/navigation_button_pressed';
import BottomSheet from '@screens/bottom_sheet';
import {dismissBottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {isSystemMessage} from '@utils/post';
import AppBindingsPostOptions from './options/app_bindings_post_option';
@ -48,6 +54,9 @@ const PostOptions = ({
sourceScreen, post, thread, bindings, serverUrl,
}: PostOptionsProps) => {
const managedConfig = useManagedConfig<ManagedConfig>();
const {bottom} = useSafeAreaInsets();
const isTablet = useIsTablet();
const Scroll = useMemo(() => (isTablet ? ScrollView : BottomSheetScrollView), [isTablet]);
const close = () => {
return dismissBottomSheet(Screens.POST_OPTIONS);
@ -63,17 +72,31 @@ const PostOptions = ({
const shouldRenderFollow = !(sourceScreen !== Screens.CHANNEL || !thread);
const shouldShowBindings = bindings.length > 0 && !isSystemPost;
const snapPoints = [
const snapPoints = useMemo(() => {
const items: BottomSheetProps['snapPoints'] = [1];
const optionsCount = [
canCopyPermalink, canCopyText, canDelete, canEdit,
canMarkAsUnread, canPin, canReply, !isSystemPost, shouldRenderFollow,
].reduce((acc, v) => {
return v ? acc + 1 : acc;
}, 0) + (shouldShowBindings ? 0.5 : 0);
items.push(bottomSheetSnapPoint(optionsCount, ITEM_HEIGHT, bottom) + (canAddReaction ? REACTION_PICKER_HEIGHT + REACTION_PICKER_MARGIN : 0));
if (shouldShowBindings) {
items.push('90%');
}
return items;
}, [
canAddReaction, canCopyPermalink, canCopyText,
canDelete, canEdit, shouldRenderFollow,
canMarkAsUnread, canPin, canReply, !isSystemPost,
].reduce((acc, v) => {
return v ? acc + 1 : acc;
}, 0);
canDelete, canEdit, shouldRenderFollow, shouldShowBindings,
canMarkAsUnread, canPin, canReply, isSystemPost, bottom,
]);
const renderContent = () => {
return (
<>
<Scroll bounces={false}>
{canAddReaction &&
<ReactionBar
bottomSheetId={Screens.POST_OPTIONS}
@ -147,31 +170,17 @@ const PostOptions = ({
bindings={bindings}
/>
}
</>
</Scroll>
);
};
const finalSnapPoints = useMemo(() => {
const additionalSnapPoints = 2;
const lowerSnapPoints = snapPoints + additionalSnapPoints;
if (!shouldShowBindings) {
return [lowerSnapPoints * ITEM_HEIGHT, 10];
}
const upperSnapPoints = lowerSnapPoints + bindings.length;
return [upperSnapPoints * ITEM_HEIGHT, lowerSnapPoints * ITEM_HEIGHT, 10];
}, [snapPoints, shouldShowBindings, bindings.length]);
const initialSnapIndex = shouldShowBindings ? 1 : 0;
return (
<BottomSheet
renderContent={renderContent}
closeButtonId={POST_OPTIONS_BUTTON}
componentId={Screens.POST_OPTIONS}
initialSnapIndex={initialSnapIndex}
snapPoints={finalSnapPoints}
initialSnapIndex={1}
snapPoints={snapPoints}
testID='post_options'
/>
);

View file

@ -4,6 +4,7 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {Screens} from '@constants';
import {useIsTablet} from '@hooks/device';
import BottomSheet from '@screens/bottom_sheet';
import {getEmojiFirstAlias} from '@utils/emoji/helpers';
@ -20,8 +21,10 @@ type Props = {
}
const Reactions = ({initialEmoji, location, reactions}: Props) => {
const isTablet = useIsTablet();
const [sortedReactions, setSortedReactions] = useState(Array.from(new Set(reactions?.map((r) => getEmojiFirstAlias(r.emojiName)))));
const [index, setIndex] = useState(sortedReactions.indexOf(initialEmoji));
const reactionsByName = useMemo(() => {
return reactions?.reduce((acc, reaction) => {
const emojiAlias = getEmojiFirstAlias(reaction.emojiName);
@ -59,6 +62,7 @@ const Reactions = ({initialEmoji, location, reactions}: Props) => {
key={emojiAlias}
location={location}
reactions={reactionsByName.get(emojiAlias)!}
type={isTablet ? 'FlatList' : 'BottomSheetFlatList'}
/>
</>
);
@ -81,7 +85,7 @@ const Reactions = ({initialEmoji, location, reactions}: Props) => {
closeButtonId='close-post-reactions'
componentId={Screens.REACTIONS}
initialSnapIndex={1}
snapPoints={['90%', '50%', 10]}
snapPoints={[1, '50%', '90%']}
testID='reactions'
/>
);

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {ListRenderItemInfo, NativeScrollEvent, NativeSyntheticEvent, PanResponder} from 'react-native';
import {FlatList} from 'react-native-gesture-handler';
@ -15,9 +16,10 @@ import type ReactionModel from '@typings/database/models/servers/reaction';
type Props = {
location: string;
reactions: ReactionModel[];
type?: BottomSheetList;
}
const ReactorsList = ({location, reactions}: Props) => {
const ReactorsList = ({location, reactions, type = 'FlatList'}: Props) => {
const serverUrl = useServerUrl();
const [enabled, setEnabled] = useState(false);
const [direction, setDirection] = useState<'down' | 'up'>('down');
@ -56,6 +58,17 @@ const ReactorsList = ({location, reactions}: Props) => {
fetchUsersByIds(serverUrl, userIds);
}, []);
if (type === 'BottomSheetFlatList') {
return (
<BottomSheetFlatList
data={reactions}
renderItem={renderItem}
overScrollMode={'always'}
testID='reactions.reactors_list.flat_list'
/>
);
}
return (
<FlatList
data={reactions}

View file

@ -133,8 +133,8 @@ const ThreadOptions = ({
renderContent={renderContent}
closeButtonId={THREAD_OPTIONS_BUTTON}
componentId={Screens.THREAD_OPTIONS}
initialSnapIndex={0}
snapPoints={[snapPoint, 10]}
initialSnapIndex={1}
snapPoints={[1, snapPoint]}
testID='thread_options'
/>
);

View file

@ -12,6 +12,7 @@ import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {getLocaleFromLanguage} from '@i18n';
import BottomSheet from '@screens/bottom_sheet';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {getUserCustomStatus, getUserTimezone, isCustomStatusExpired} from '@utils/user';
import UserProfileCustomStatus from './custom_status';
@ -45,7 +46,6 @@ const TITLE_HEIGHT = 118;
const OPTIONS_HEIGHT = 82;
const SINGLE_OPTION_HEIGHT = 68;
const LABEL_HEIGHT = 58;
const EXTRA_HEIGHT = 60;
const UserProfile = ({
channelId, closeButtonId, currentUserId, enablePostIconOverride, enablePostUsernameOverride,
@ -55,14 +55,14 @@ const UserProfile = ({
}: Props) => {
const {formatMessage, locale} = useIntl();
const serverUrl = useServerUrl();
const insets = useSafeAreaInsets();
const {bottom} = useSafeAreaInsets();
const channelContext = [Screens.CHANNEL, Screens.THREAD].includes(location);
const showOptions: OptionsType = channelContext && !user.isBot ? 'all' : 'message';
const override = Boolean(userIconOverride || usernameOverride);
const timezone = getUserTimezone(user);
const customStatus = getUserCustomStatus(user);
const showCustomStatus = isCustomStatusEnabled && Boolean(customStatus) && !user.isBot && !isCustomStatusExpired(user);
let localTime: string|undefined;
if (timezone) {
moment.locale(getLocaleFromLanguage(locale).toLowerCase());
let format = 'H:mm';
@ -73,36 +73,42 @@ const UserProfile = ({
localTime = mtz.tz(Date.now(), timezone).format(format);
}
const showCustomStatus = isCustomStatusEnabled && Boolean(customStatus) && !user.isBot && !isCustomStatusExpired(user);
const showUserProfileOptions = (!isDirectMessage || !channelContext) && !override;
const showNickname = Boolean(user.nickname) && !override && !user.isBot;
const showPosition = Boolean(user.position) && !override && !user.isBot;
const showLocalTime = Boolean(localTime) && !override && !user.isBot;
const snapPoints = useMemo(() => {
let initial = TITLE_HEIGHT;
if ((!isDirectMessage || !channelContext) && !override) {
initial += showOptions === 'all' ? OPTIONS_HEIGHT : SINGLE_OPTION_HEIGHT;
let title = TITLE_HEIGHT;
if (showUserProfileOptions) {
title += showOptions === 'all' ? OPTIONS_HEIGHT : SINGLE_OPTION_HEIGHT;
}
let labels = 0;
if (!override && !user.isBot) {
if (showCustomStatus) {
labels += 1;
}
if (user.nickname) {
labels += 1;
}
if (user.position) {
labels += 1;
}
if (localTime) {
labels += 1;
}
initial += (labels * LABEL_HEIGHT);
if (showCustomStatus) {
labels += 1;
}
return [initial + insets.bottom + EXTRA_HEIGHT, 10];
if (showNickname) {
labels += 1;
}
if (showPosition) {
labels += 1;
}
if (showLocalTime) {
labels += 1;
}
return [
1,
bottomSheetSnapPoint(labels, LABEL_HEIGHT, bottom) + title,
];
}, [
isChannelAdmin, isDirectMessage, isSystemAdmin,
isTeamAdmin, user, localTime, insets.bottom, override,
showUserProfileOptions, showCustomStatus, showNickname,
showPosition, showLocalTime, bottom,
]);
useEffect(() => {
@ -125,7 +131,7 @@ const UserProfile = ({
userIconOverride={userIconOverride}
usernameOverride={usernameOverride}
/>
{(!isDirectMessage || !channelContext) && !override &&
{showUserProfileOptions &&
<UserProfileOptions
location={location}
type={showOptions}
@ -134,21 +140,21 @@ const UserProfile = ({
/>
}
{showCustomStatus && <UserProfileCustomStatus customStatus={customStatus!}/>}
{Boolean(user.nickname) && !override && !user.isBot &&
{showNickname &&
<UserProfileLabel
description={user.nickname}
testID='user_profile.nickname'
title={formatMessage({id: 'channel_info.nickname', defaultMessage: 'Nickname'})}
/>
}
{Boolean(user.position) && !override && !user.isBot &&
{showPosition &&
<UserProfileLabel
description={user.position}
testID='user_profile.position'
title={formatMessage({id: 'channel_info.position', defaultMessage: 'Position'})}
/>
}
{Boolean(localTime) && !override && !user.isBot &&
{showLocalTime &&
<UserProfileLabel
description={localTime!}
testID='user_profile.local_time'
@ -164,7 +170,7 @@ const UserProfile = ({
renderContent={renderContent}
closeButtonId={closeButtonId}
componentId={Screens.USER_PROFILE}
initialSnapIndex={0}
initialSnapIndex={1}
snapPoints={snapPoints}
testID='user_profile'
/>

View file

@ -6,7 +6,7 @@ import {NativeModules, Platform} from 'react-native';
import {Device} from '@constants';
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
import {IOS_STATUS_BAR_HEIGHT} from '@constants/view';
import {STATUS_BAR_HEIGHT} from '@constants/view';
const {MattermostManaged} = NativeModules;
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
@ -141,12 +141,9 @@ export async function isTablet() {
export const pluckUnique = (key: string) => (array: Array<{[key: string]: unknown}>) => Array.from(new Set(array.map((obj) => obj[key])));
export function bottomSheetSnapPoint(itemsCount: number, itemHeight: number, bottomInset = 0) {
let bottom = bottomInset;
if (!bottom && Platform.OS === 'ios') {
bottom = IOS_STATUS_BAR_HEIGHT;
}
return ((itemsCount + Platform.select({android: 1, default: 0})) * itemHeight) + (bottom * 2.5);
export function bottomSheetSnapPoint(itemsCount: number, itemHeight: number, bottomInset: number) {
const bottom = Platform.select({ios: bottomInset, default: 0}) + STATUS_BAR_HEIGHT;
return (itemsCount * itemHeight) + bottom;
}
export function hasTrailingSpaces(term: string) {

View file

@ -1,32 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ScaledSize} from 'react-native';
import {EdgeInsets} from 'react-native-safe-area-context';
import {ITEM_HEIGHT} from '@components/team_list/team_list_item/team_list_item';
import {PADDING_TOP_MOBILE} from '@screens/bottom_sheet';
import {TITLE_HEIGHT, TITLE_SEPARATOR_MARGIN} from '@screens/bottom_sheet/content';
import {bottomSheetSnapPoint} from '@utils/helpers';
import type TeamModel from '@typings/database/models/servers/team';
type TeamsSnapProps = {
teams: TeamModel[];
dimensions: ScaledSize;
insets: EdgeInsets;
}
const NO_TEAMS_HEIGHT = 392;
export const getTeamsSnapHeight = ({dimensions, teams, insets}: TeamsSnapProps) => {
let height = NO_TEAMS_HEIGHT;
if (teams.length) {
const itemsHeight = bottomSheetSnapPoint(teams.length, ITEM_HEIGHT, 0);
const heightWithHeader = PADDING_TOP_MOBILE +
TITLE_HEIGHT + (TITLE_SEPARATOR_MARGIN * 2) +
itemsHeight + insets.bottom;
const maxHeight = Math.round((dimensions.height * 0.9));
height = Math.min(maxHeight, heightWithHeader);
}
return height;
};

63
package-lock.json generated
View file

@ -16,6 +16,7 @@
"@formatjs/intl-numberformat": "8.3.3",
"@formatjs/intl-pluralrules": "5.1.8",
"@formatjs/intl-relativetimeformat": "11.1.8",
"@gorhom/bottom-sheet": "4.4.5",
"@mattermost/compass-icons": "0.1.35",
"@mattermost/react-native-emm": "1.3.3",
"@mattermost/react-native-network-client": "1.0.2",
@ -94,7 +95,6 @@
"react-native-webview": "11.26.0",
"react-syntax-highlighter": "15.5.0",
"readable-stream": "3.6.0",
"reanimated-bottom-sheet": "1.0.0-alpha.22",
"semver": "7.3.8",
"serialize-error": "11.0.0",
"shallow-equals": "1.0.0",
@ -2349,6 +2349,33 @@
"tslib": "^2.4.0"
}
},
"node_modules/@gorhom/bottom-sheet": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.5.tgz",
"integrity": "sha512-Z5Z20wshLUB8lIdtMKoJaRnjd64wBR/q8EeVPThrg+skrcBwBPHfUwZJ2srB0rEszA/01ejSJy/ixyd7Ra7vUA==",
"dependencies": {
"@gorhom/portal": "1.0.14",
"invariant": "^2.2.4"
},
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-gesture-handler": ">=1.10.1",
"react-native-reanimated": ">=2.2.0"
}
},
"node_modules/@gorhom/portal": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz",
"integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==",
"dependencies": {
"nanoid": "^3.3.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@ -19082,17 +19109,6 @@
"resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz",
"integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg=="
},
"node_modules/reanimated-bottom-sheet": {
"version": "1.0.0-alpha.22",
"resolved": "https://registry.npmjs.org/reanimated-bottom-sheet/-/reanimated-bottom-sheet-1.0.0-alpha.22.tgz",
"integrity": "sha512-NxecCn+2iA4YzkFuRK5/b86GHHS2OhZ9VRgiM4q18AC20YE/psRilqxzXCKBEvkOjP5AaAvY0yfE7EkEFBjTvw==",
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-gesture-handler": "*",
"react-native-reanimated": "*"
}
},
"node_modules/recast": {
"version": "0.20.5",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.20.5.tgz",
@ -23595,6 +23611,23 @@
"tslib": "^2.4.0"
}
},
"@gorhom/bottom-sheet": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.5.tgz",
"integrity": "sha512-Z5Z20wshLUB8lIdtMKoJaRnjd64wBR/q8EeVPThrg+skrcBwBPHfUwZJ2srB0rEszA/01ejSJy/ixyd7Ra7vUA==",
"requires": {
"@gorhom/portal": "1.0.14",
"invariant": "^2.2.4"
}
},
"@gorhom/portal": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz",
"integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==",
"requires": {
"nanoid": "^3.3.1"
}
},
"@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@ -36257,12 +36290,6 @@
"resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz",
"integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg=="
},
"reanimated-bottom-sheet": {
"version": "1.0.0-alpha.22",
"resolved": "https://registry.npmjs.org/reanimated-bottom-sheet/-/reanimated-bottom-sheet-1.0.0-alpha.22.tgz",
"integrity": "sha512-NxecCn+2iA4YzkFuRK5/b86GHHS2OhZ9VRgiM4q18AC20YE/psRilqxzXCKBEvkOjP5AaAvY0yfE7EkEFBjTvw==",
"requires": {}
},
"recast": {
"version": "0.20.5",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.20.5.tgz",

View file

@ -13,6 +13,7 @@
"@formatjs/intl-numberformat": "8.3.3",
"@formatjs/intl-pluralrules": "5.1.8",
"@formatjs/intl-relativetimeformat": "11.1.8",
"@gorhom/bottom-sheet": "4.4.5",
"@mattermost/compass-icons": "0.1.35",
"@mattermost/react-native-emm": "1.3.3",
"@mattermost/react-native-network-client": "1.0.2",
@ -91,7 +92,6 @@
"react-native-webview": "11.26.0",
"react-syntax-highlighter": "15.5.0",
"readable-stream": "3.6.0",
"reanimated-bottom-sheet": "1.0.0-alpha.22",
"semver": "7.3.8",
"serialize-error": "11.0.0",
"shallow-equals": "1.0.0",

4
types/components/bottom_sheet.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
type BottomSheetList = 'FlatList' | 'BottomSheetFlatList';