Merge branch 'main' into MM-47655-add-people-screen-main

This commit is contained in:
Jason Frerich
2023-01-05 16:24:44 -06:00
58 changed files with 881 additions and 484 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

@@ -6,6 +6,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
@@ -20,8 +21,12 @@ import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.mattermost.helpers.Credentials;
import com.reactlibrary.createthumbnail.CreateThumbnailModule;
import com.mattermost.helpers.RealPathUtil;
import java.io.File;
@@ -29,6 +34,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.channels.FileChannel;
public class MattermostManagedModule extends ReactContextBaseJavaModule {
@@ -206,6 +212,30 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void createThumbnail(ReadableMap options, Promise promise) {
try {
WritableMap optionsMap = Arguments.createMap();
optionsMap.merge(options);
String url = options.hasKey("url") ? options.getString("url") : "";
URL videoUrl = new URL(url);
String serverUrl = videoUrl.getProtocol() + "://" + videoUrl.getHost() + ":" + videoUrl.getPort();
String token = Credentials.getCredentialsForServerSync(this.reactContext, serverUrl);
if (!TextUtils.isEmpty(token)) {
WritableMap headers = Arguments.createMap();
if (optionsMap.hasKey("headers")) {
headers.merge(optionsMap.getMap("headers"));
}
headers.putString("Authorization", "Bearer " + token);
optionsMap.putMap("headers", headers);
}
CreateThumbnailModule thumb = new CreateThumbnailModule(this.reactContext);
thumb.create(optionsMap.copy(), promise);
} catch (Exception e) {
promise.reject("CreateThumbnail_ERROR", e);
}
}
private static class SaveDataTask extends GuardedResultAsyncTask<Object> {
private final WeakReference<Context> weakContext;
private final String fromFile;

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

@@ -2,11 +2,10 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {StyleSheet, useWindowDimensions, View} from 'react-native';
import {createThumbnail} from 'react-native-create-thumbnail';
import {StyleSheet, useWindowDimensions, View, NativeModules} from 'react-native';
import {updateLocalFile} from '@actions/local/file';
import {buildFilePreviewUrl, fetchPublicLink} from '@actions/remote/file';
import {buildFilePreviewUrl, buildFileUrl} from '@actions/remote/file';
import CompassIcon from '@components/compass_icon';
import ProgressiveImage from '@components/progressive_image';
import {useServerUrl} from '@context/server';
@@ -18,6 +17,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import FileIcon from './file_icon';
import type {ResizeMode} from 'react-native-fast-image';
const {createThumbnail} = NativeModules.MattermostManaged;
type Props = {
index: number;
@@ -84,11 +84,9 @@ const VideoFile = ({
try {
const exists = data.mini_preview ? await fileExists(data.mini_preview) : false;
if (!data.mini_preview || !exists) {
// We use the public link to avoid having to pass the token through a third party
// library
const publicUri = await fetchPublicLink(serverUrl, data.id!);
if (('link') in publicUri) {
const {path: uri, height, width} = await createThumbnail({url: data.localPath || publicUri.link, timeStamp: 2000});
const videoUrl = buildFileUrl(serverUrl, data.id!);
if (videoUrl) {
const {path: uri, height, width} = await createThumbnail({url: data.localPath || videoUrl, timeStamp: 2000});
data.mini_preview = uri;
data.height = height;
data.width = width;

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

@@ -65,7 +65,7 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => {
style: 'destructive',
onPress: async () => {
await deleteFileCache(serverUrl);
await getAllCachedFiles();
getAllCachedFiles();
},
},
],

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

@@ -166,32 +166,41 @@ export async function deleteV1Data() {
export async function deleteFileCache(serverUrl: string) {
const serverDir = urlSafeBase64Encode(serverUrl);
deleteFileCacheByDir(serverDir);
return deleteFileCacheByDir(serverDir);
}
export async function deleteLegacyFileCache(serverUrl: string) {
const serverDir = hashCode_DEPRECATED(serverUrl);
deleteFileCacheByDir(serverDir);
return deleteFileCacheByDir(serverDir);
}
export async function deleteFileCacheByDir(dir: string) {
if (Platform.OS === 'ios') {
const appGroupCacheDir = `${getIOSAppGroupDetails().appGroupSharedDirectory}/Library/Caches/${dir}`;
await deleteFilesInDir(appGroupCacheDir);
}
const cacheDir = `${FileSystem.CachesDirectoryPath}/${dir}`;
if (cacheDir) {
const cacheDirInfo = await FileSystem.exists(cacheDir);
await deleteFilesInDir(cacheDir);
return true;
}
async function deleteFilesInDir(directory: string) {
if (directory) {
const cacheDirInfo = await FileSystem.exists(directory);
if (cacheDirInfo) {
if (Platform.OS === 'ios') {
await FileSystem.unlink(cacheDir);
await FileSystem.mkdir(cacheDir);
await FileSystem.unlink(directory);
await FileSystem.mkdir(directory);
} else {
const lstat = await FileSystem.readDir(cacheDir);
const lstat = await FileSystem.readDir(directory);
lstat.forEach((stat: FileSystem.ReadDirItem) => {
FileSystem.unlink(stat.path);
});
}
}
}
return true;
}
export function lookupMimeType(filename: string) {
@@ -525,8 +534,18 @@ export const getAllFilesInCachesDirectory = async (serverUrl: string) => {
try {
const files: FileSystem.ReadDirItem[][] = [];
const directoryFiles = await FileSystem.readDir(`${FileSystem.CachesDirectoryPath}/${urlSafeBase64Encode(serverUrl)}`);
files.push(directoryFiles);
const promises = [FileSystem.readDir(`${FileSystem.CachesDirectoryPath}/${urlSafeBase64Encode(serverUrl)}`)];
if (Platform.OS === 'ios') {
const cacheDir = `${getIOSAppGroupDetails().appGroupSharedDirectory}/Library/Caches/${urlSafeBase64Encode(serverUrl)}`;
promises.push(FileSystem.readDir(cacheDir));
}
const dirs = await Promise.allSettled(promises);
dirs.forEach((p) => {
if (p.status === 'fulfilled') {
files.push(p.value);
}
});
const flattenedFiles = files.flat();
const totalSize = flattenedFiles.reduce((acc, file) => acc + file.size, 0);

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;
};

View File

@@ -320,32 +320,30 @@ function generateTitle() {
const platform = IOS === 'true' ? 'iOS' : 'Android';
const lane = `${platform} Build`;
const appExtension = IOS === 'true' ? 'ipa' : 'apk';
const appFileName = TYPE === 'GEKIDOU' ? `Mattermost_Beta.${appExtension}` : `Mattermost.${appExtension}`;
let buildLink = ` with [${lane}:${COMMIT_HASH}](https://pr-builds.mattermost.com/mattermost-mobile/${BRANCH}-${COMMIT_HASH}/${appFileName})`;
if (RELEASE_VERSION && RELEASE_BUILD_NUMBER) {
const releaseType = TYPE === 'GEKIDOU' ? 'mattermost-mobile-beta' : 'mattermost-mobile';
buildLink = ` with [${RELEASE_VERSION}:${RELEASE_BUILD_NUMBER}](https://releases.mattermost.com/${releaseType}/${RELEASE_VERSION}/${RELEASE_BUILD_NUMBER}/${appFileName})`;
}
const appFileName = `Mattermost_Beta.${appExtension}`;
const appBuildType = 'mattermost-mobile-beta';
let buildLink = '';
let releaseDate = '';
if (RELEASE_DATE) {
releaseDate = ` for ${RELEASE_DATE}`;
}
let title;
switch (TYPE) {
case 'PR':
buildLink = ` with [${lane}:${COMMIT_HASH}](https://pr-builds.mattermost.com/${appBuildType}/${BRANCH}-${COMMIT_HASH}/${appFileName})`;
title = `${platform} E2E for Pull Request Build: [${BRANCH}](${PULL_REQUEST})${buildLink}`;
break;
case 'RELEASE':
if (RELEASE_VERSION && RELEASE_BUILD_NUMBER) {
buildLink = ` with [${RELEASE_VERSION}:${RELEASE_BUILD_NUMBER}](https://releases.mattermost.com/${appBuildType}/${RELEASE_VERSION}/${RELEASE_BUILD_NUMBER}/${appFileName})`;
}
if (RELEASE_DATE) {
releaseDate = ` for ${RELEASE_DATE}`;
}
title = `${platform} E2E for Release Build${buildLink}${releaseDate}`;
break;
case 'MASTER':
title = `${platform} E2E for Master Nightly Build (Prod tests)${buildLink}`;
break;
case 'GEKIDOU':
title = `${platform} E2E for Gekidou Nightly Build (Prod tests)${buildLink}`;
case 'MAIN':
title = `${platform} E2E for Main Nightly Build (Prod tests)${buildLink}`;
break;
default:
title = `${platform} E2E for Build${buildLink}`;

View File

@@ -0,0 +1,64 @@
//
// FileCache.swift
// Gekidou
//
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//
import Foundation
import UIKit
public class FileCache: NSObject {
private var cacheURL: URL?
@objc public static let `default` = FileCache()
override private init() {
super.init()
let filemgr = FileManager.default
let appGroupId = Bundle.main.infoDictionary!["AppGroupIdentifier"] as! String
let containerUrl = filemgr.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)
if let url = containerUrl,
let cacheURL = url.appendingPathComponent("Library", isDirectory: true) as URL? {
self.cacheURL = cacheURL.appendingPathComponent("Caches", isDirectory: true)
self.createDirectoryIfNeeded(directory: self.cacheURL)
}
}
private func createDirectoryIfNeeded(directory: URL?) {
var isDirectory = ObjCBool(false)
if let cachePath = directory?.path {
let exists = FileManager.default.fileExists(atPath: cachePath, isDirectory: &isDirectory)
if !exists && !isDirectory.boolValue {
try? FileManager.default.createDirectory(atPath: cachePath, withIntermediateDirectories: true, attributes: nil)
}
}
}
private func getUrlImageFor(serverUrl: String, userId: String) -> URL? {
guard let url = cacheURL else {return nil}
let serverCacheURL = url.appendingPathComponent(serverUrl.toUrlSafeBase64Encode(), isDirectory: true)
createDirectoryIfNeeded(directory: serverCacheURL)
return serverCacheURL.appendingPathComponent(userId + ".png")
}
public func getProfileImage(serverUrl: String, userId: String) -> UIImage? {
guard let url = getUrlImageFor(serverUrl: serverUrl, userId: userId) else { return nil }
return UIImage(contentsOfFile: url.path)
}
public func saveProfileImage(serverUrl: String, userId: String, imageData: Data?) {
guard let data = imageData,
let url = getUrlImageFor(serverUrl: serverUrl, userId: userId)
else { return }
do {
try data.write(to: url)
} catch let error {
print("Erro saving image. \(error)")
}
}
}

View File

@@ -1,6 +1,7 @@
import Foundation
import UserNotifications
import SQLite
import os.log
public struct AckNotification: Codable {
let type: String
@@ -66,6 +67,14 @@ extension String {
guard self.hasPrefix(prefix) else { return self }
return String(self.dropFirst(prefix.count))
}
func toUrlSafeBase64Encode() -> String {
return Data(
self.replacingOccurrences(of: "/\\+/g", with: "-", options: .regularExpression)
.replacingOccurrences(of: "/\\//g", with: "_", options: .regularExpression)
.utf8
).base64EncodedString()
}
}
extension Network {
@@ -93,8 +102,18 @@ extension Network {
let semaphore = DispatchSemaphore(value: 0)
func processResponse(data: Data?, response: URLResponse?, error: Error?) {
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 && error == nil {
imgData = data
if let httpResponse = response as? HTTPURLResponse {
if (httpResponse.statusCode == 200 && error == nil) {
imgData = data
FileCache.default.saveProfileImage(serverUrl: serverUrl, userId: senderId, imageData: data)
} else {
os_log(
OSLogType.default,
"Mattermost Notifications: Request for profile image failed with status %{public}@ and error %{public}@",
httpResponse.statusCode,
(error?.localizedDescription ?? "")
)
}
}
semaphore.signal()
}
@@ -103,9 +122,14 @@ extension Network {
let url = URL(string: overrideUrl) {
request(url, withMethod: "GET", withServerUrl: "", completionHandler: processResponse)
} else {
let endpoint = "/users/\(senderId)/image"
let url = buildApiUrl(serverUrl, endpoint)
request(url, withMethod: "GET", withServerUrl: serverUrl, completionHandler: processResponse)
if let image = FileCache.default.getProfileImage(serverUrl: serverUrl, userId: senderId) {
os_log(OSLogType.default, "Mattermost Notifications: cached image")
imgData = image.pngData()
semaphore.signal()
} else {
os_log(OSLogType.default, "Mattermost Notifications: image not cached")
fetchUserProfilePicture(userId: senderId, withServerUrl: serverUrl, completionHandler: processResponse)
}
}
semaphore.wait()

View File

@@ -27,4 +27,12 @@ import Gekidou
@objc func setPreference(_ value: Any?, forKey name: String) {
Preferences.default.set(value, forKey: name)
}
@objc func getToken(for url: String) -> String? {
if let token = try? Keychain.default.getToken(for: url) {
return token
}
return nil
}
}

View File

@@ -8,6 +8,8 @@
#import "AppDelegate.h"
#import "MattermostManaged.h"
#import "CreateThumbnail.h"
#import "Mattermost-Swift.h"
@implementation MattermostManaged
@@ -107,7 +109,6 @@ RCT_EXPORT_METHOD(renameDatabase: (NSString *)databaseName to: (NSString *) new
NSDictionary *appGroupDir = [self appGroupSharedDirectory];
NSString *databaseDir;
NSString *newDBDir;
if(databaseName){
databaseDir = [NSString stringWithFormat:@"%@/%@%@", appGroupDir[@"databasePath"], databaseName , @".db"];
@@ -200,4 +201,27 @@ RCT_EXPORT_METHOD(lockPortrait)
}
RCT_EXPORT_METHOD(createThumbnail:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
NSMutableDictionary *newConfig = [config mutableCopy];
NSMutableDictionary *headers = [config[@"headers"] ?: @{} mutableCopy];
NSString *url = (NSString *)[config objectForKey:@"url"] ?: @"";
NSURL *vidURL = nil;
NSString *url_ = [url lowercaseString];
if ([url_ hasPrefix:@"http://"] || [url_ hasPrefix:@"https://"] || [url_ hasPrefix:@"file://"]) {
vidURL = [NSURL URLWithString:url];
NSString *serverUrl = [NSString stringWithFormat:@"%@://%@:%@", vidURL.scheme, vidURL.host, vidURL.port];
if (vidURL != nil) {
NSString *token = [[GekidouWrapper default] getTokenFor:serverUrl];
if (token != nil) {
headers[@"Authorization"] = [NSString stringWithFormat:@"Bearer %@", token];
newConfig[@"headers"] = headers;
}
}
}
[CreateThumbnail create:newConfig findEventsWithResolver:resolve rejecter:reject];
}
@end

View File

@@ -1,6 +1,7 @@
import Gekidou
import UserNotifications
import Intents
import os.log
class NotificationService: UNNotificationServiceExtension {
let preferences = Gekidou.Preferences.default
@@ -24,13 +25,24 @@ class NotificationService: UNNotificationServiceExtension {
let ackNotification = try? JSONDecoder().decode(AckNotification.self, from: jsonData) {
fetchReceipt(ackNotification)
} else {
os_log(OSLogType.default, "Mattermost Notifications: bestAttemptContent seems to be empty, will call sendMessageIntent")
sendMessageIntent(notification: request.content)
}
}
func processResponse(serverUrl: String, data: Data, bestAttemptContent: UNMutableNotificationContent) {
bestAttemptContent.userInfo["server_url"] = serverUrl
os_log(
OSLogType.default,
"Mattermost Notifications: process receipt response for serverUrl %{public}@",
serverUrl
)
let json = try? JSONSerialization.jsonObject(with: data) as! [String: Any]
os_log(
OSLogType.default,
"Mattermost Notifications: parsed json response %{public}@",
String(describing: json != nil)
)
if let json = json {
if let message = json["message"] as? String {
bestAttemptContent.body = message
@@ -49,10 +61,12 @@ class NotificationService: UNNotificationServiceExtension {
if (preferences.object(forKey: "ApplicationIsForeground") as? String != "true") {
Network.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: {[weak self] notification in
os_log(OSLogType.default, "Mattermost Notifications: processed data for db. Will call sendMessageIntent")
self?.sendMessageIntent(notification: bestAttemptContent)
})
} else {
bestAttemptContent.badge = Gekidou.Database.default.getTotalMentions() as NSNumber
os_log(OSLogType.default, "Mattermost Notifications: app in the foreground, no data processed. Will call sendMessageIntent")
sendMessageIntent(notification: bestAttemptContent)
}
}
@@ -60,7 +74,9 @@ class NotificationService: UNNotificationServiceExtension {
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let bestAttemptContent = bestAttemptContent {
os_log(OSLogType.default, "Mattermost Notifications: service extension time expired")
if let bestAttemptContent = bestAttemptContent {
os_log(OSLogType.default, "Mattermost Notifications: calling sendMessageIntent before expiration")
sendMessageIntent(notification: bestAttemptContent)
}
}
@@ -71,68 +87,78 @@ class NotificationService: UNNotificationServiceExtension {
let channelId = notification.userInfo["channel_id"] as! String
let rootId = notification.userInfo.index(forKey: "root_id") != nil ? notification.userInfo["root_id"] as! String : ""
let senderId = notification.userInfo["sender_id"] as? String ?? ""
let senderName = notification.userInfo["sender_name"] as? String ?? ""
let channelName = notification.userInfo["channel_name"] as? String ?? ""
let overrideIconUrl = notification.userInfo["override_icon_url"] as? String
let serverUrl = notification.userInfo["server_url"] as? String ?? ""
let message = (notification.userInfo["message"] as? String ?? "")
let avatarData = Network.default.fetchProfileImageSync(serverUrl, senderId: senderId, overrideIconUrl: overrideIconUrl)
let handle = INPersonHandle(value: notification.userInfo["sender_id"] as? String, type: .unknown)
var avatar: INImage?
if let imgData = avatarData {
avatar = INImage(imageData: imgData)
}
let sender = INPerson(personHandle: handle,
nameComponents: nil,
displayName: channelName,
image: avatar,
contactIdentifier: nil,
customIdentifier: nil)
var conversationId = channelId
if isCRTEnabled && rootId != "" {
conversationId = rootId
}
let intent = INSendMessageIntent(recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: message,
speakableGroupName: nil,
conversationIdentifier: conversationId,
serviceName: nil,
sender: sender,
attachments: nil)
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate { error in
if error != nil {
self.contentHandler?(notification)
return
}
do {
let updatedContent = try notification.updating(from: intent)
self.contentHandler?(updatedContent)
} catch {
self.contentHandler?(notification)
if senderId != "" && serverUrl != "" {
os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, senderId)
let avatarData = Network.default.fetchProfileImageSync(serverUrl, senderId: senderId, overrideIconUrl: overrideIconUrl)
if let imgData = avatarData,
let avatar = INImage(imageData: imgData) as INImage? {
os_log(OSLogType.default, "Mattermost Notifications: creating intent")
var conversationId = channelId
if isCRTEnabled && rootId != "" {
conversationId = rootId
}
let handle = INPersonHandle(value: senderId, type: .unknown)
let sender = INPerson(personHandle: handle,
nameComponents: nil,
displayName: channelName,
image: avatar,
contactIdentifier: nil,
customIdentifier: nil)
let intent = INSendMessageIntent(recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: message,
speakableGroupName: nil,
conversationIdentifier: conversationId,
serviceName: nil,
sender: sender,
attachments: nil)
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate { error in
if error != nil {
self.contentHandler?(notification)
os_log(OSLogType.default, "Mattermost Notifications: sendMessageIntent intent error %{public}@", error! as CVarArg)
}
do {
let updatedContent = try notification.updating(from: intent)
os_log(OSLogType.default, "Mattermost Notifications: present updated notification")
self.contentHandler?(updatedContent)
} catch {
os_log(OSLogType.default, "Mattermost Notifications: something failed updating the notification %{public}@", error as CVarArg)
self.contentHandler?(notification)
}
}
}
}
} else {
os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification")
self.contentHandler?(notification)
}
}
func fetchReceipt(_ ackNotification: AckNotification) -> Void {
if (self.retryIndex >= self.fibonacciBackoffsInSeconds.count) {
os_log(OSLogType.default, "Mattermost Notifications: max retries reached. Will call sendMessageIntent")
sendMessageIntent(notification: bestAttemptContent!)
return
}
Network.default.postNotificationReceipt(ackNotification) {data, response, error in
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
os_log(
OSLogType.default,
"Mattermost Notifications: notification receipt failed with status %{public}@. Will call sendMessageIntent",
httpResponse.statusCode
)
self.sendMessageIntent(notification: self.bestAttemptContent!)
return
}
@@ -143,6 +169,11 @@ class NotificationService: UNNotificationServiceExtension {
let backoffInSeconds = self.fibonacciBackoffsInSeconds[self.retryIndex]
DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {
os_log(
OSLogType.default,
"Mattermost Notifications: receipt retrieval failed. Retry %{public}@",
self.retryIndex
)
self.fetchReceipt(ackNotification)
})

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",

View File

@@ -0,0 +1,53 @@
diff --git a/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.h b/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.h
index 28b1d9b..cb63c52 100644
--- a/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.h
+++ b/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.h
@@ -3,5 +3,5 @@
#import <UIKit/UIKit.h>
@interface CreateThumbnail : NSObject <RCTBridgeModule>
-
++(void)create:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject;
@end
diff --git a/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.m b/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.m
index 92cc49f..23cc83c 100644
--- a/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.m
+++ b/node_modules/react-native-create-thumbnail/ios/CreateThumbnail.m
@@ -6,6 +6,10 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(create:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
+ [CreateThumbnail create:config findEventsWithResolver:resolve rejecter:reject];
+}
+
++(void) create:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
NSString *url = (NSString *)[config objectForKey:@"url"] ?: @"";
int timeStamp = [[config objectForKey:@"timeStamp"] intValue] ?: 0;
NSString *format = (NSString *)[config objectForKey:@"format"] ?: @"jpeg";
@@ -82,7 +86,7 @@ RCT_EXPORT_METHOD(create:(NSDictionary *)config findEventsWithResolver:(RCTPromi
}
}
-- (unsigned long long) sizeOfFolderAtPath:(NSString *)path {
++ (unsigned long long) sizeOfFolderAtPath:(NSString *)path {
NSArray *files = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:path error:nil];
NSEnumerator *enumerator = [files objectEnumerator];
NSString *fileName;
@@ -93,7 +97,7 @@ RCT_EXPORT_METHOD(create:(NSDictionary *)config findEventsWithResolver:(RCTPromi
return size;
}
-- (void) cleanDir:(NSString *)path forSpace:(unsigned long long)size {
++ (void) cleanDir:(NSString *)path forSpace:(unsigned long long)size {
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error = nil;
unsigned long long deletedSize = 0;
@@ -110,7 +114,7 @@ RCT_EXPORT_METHOD(create:(NSDictionary *)config findEventsWithResolver:(RCTPromi
return;
}
-- (void) generateThumbImage:(AVURLAsset *)asset atTime:(int)timeStamp completion:(void (^)(UIImage* thumbnail))completion failure:(void (^)(NSError* error))failure {
++ (void) generateThumbImage:(AVURLAsset *)asset atTime:(int)timeStamp completion:(void (^)(UIImage* thumbnail))completion failure:(void (^)(NSError* error))failure {
AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
generator.appliesPreferredTrackTransform = YES;
generator.maximumSize = CGSizeMake(512, 512);

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';