forked from Ivasoft/mattermost-mobile
Merge branch 'main' into MM-47655-add-people-screen-main
This commit is contained in:
22
NOTICE.txt
22
NOTICE.txt
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,8 @@ type Props = {
|
||||
onSubmit: (data: PostPriorityData) => void;
|
||||
};
|
||||
|
||||
export const COMPONENT_HEIGHT = 200;
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(() => ([
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -65,7 +65,7 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => {
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await deleteFileCache(serverUrl);
|
||||
await getAllCachedFiles();
|
||||
getAllCachedFiles();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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}`;
|
||||
|
||||
64
ios/Gekidou/Sources/Gekidou/FileCache.swift
Normal file
64
ios/Gekidou/Sources/Gekidou/FileCache.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
63
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
53
patches/react-native-create-thumbnail+1.6.4.patch
Normal file
53
patches/react-native-create-thumbnail+1.6.4.patch
Normal 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
4
types/components/bottom_sheet.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
type BottomSheetList = 'FlatList' | 'BottomSheetFlatList';
|
||||
Reference in New Issue
Block a user