* refactor: implement custom ExpoImage wrapper for cache control Add ExpoImage component with automatic cacheKey/cachePath management and replace all expo-image imports across the app * refactor(ios): convert Gekidou to CocoaPods Migrate from Swift Package Manager to CocoaPods, add Keychain write operations, refactor notification handler to remove react-native-notifications headers, and upgrade Swift to 5.0 * npm audit * update fastlane * feat(ci): integrate Intune MAM for enterprise builds with strict OSS protection Add Intune submodule, CI actions, Fastlane configuration, developer scripts, pre-commit hooks, and validation workflows to enable internal MAM builds while protecting OSS repository * fix tests by mocking @mattermost/intune * feat: implement Intune MAM integration with comprehensive security enforcement Add IntuneManager, refactor SecurityManager/SessionManager for MAM policies, implement native OIDC auth flow, add biometric enforcement, conditional launch blocking, and file protection controls * fix alerts when no server database is present * Unify cache strategy * fix emit config changed after it was stored in the db * Handle Mid-Session Enrollment Detection * fix ADALLogOverrideDisabled missing in Fastfile * fix flow for initial enrollment * fix and add unit tests * enable Intune configuration for PR and beta builds, CLIENT_ID should be changed before actual release * Update intune submodule with addressed feedback * fix validate-intune-clean workflow * feat(intune): add comprehensive error handling and SAML+Entra support Add production-ready error handling for native Entra authentication with user-friendly i18n messages, comprehensive test coverage, and support for Entra login when server requires SAML. * update i18n * update intune submodule * update build-pr token * fix race condition between server auth and intune enrollment * fix CI workflow to build with intune * use deploy key for intune submodule * set the config directly in the submodule .git * debug injection * try setting GIT_SSH_COMMAND * remove action debug * fix server url input * match pod cache with intune hash * Fastfile and envs * have workflows check for intune/.git * have ci cache intune frameworks as well * update Fastlane to set no-cache to artifacts uploaded * fix s3 upload * fix pblist template * Attempt to remove the cache control for PR uploads to s3 * use hash from commit for S3 path * Implement crash-resilient selective wipe with automatic retry and add removeInternetPassword to Gekidou Keychain * Fix surface errors from intune login * fix postinstall scripts * use cacheKey for draft md images * remove unnecessary double await during test * Have isMinimumLicenseTier accept valid license sku tier as target * Add missing Auth error messages * remove the last period for intune errors in i18n * do not call unenroll with wipe on manual logout * Fix tests and Intune error messages * do not filter any SSO type regardless of which is used for Intune * fix 412 to not retry * fix tests, app logs sharing and share_extension avatar cache * apply setScreenCapturePolicy on license change Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * re-apply screen capture on enrollment Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * use userData from intunr login and prevent getMe Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * Check for Biometrics and Jailbreak as we used to --------- Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>
184 lines
6.8 KiB
TypeScript
184 lines
6.8 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {LinearGradient, type LinearGradientProps} from 'expo-linear-gradient';
|
|
import React, {useCallback, useMemo, useState} from 'react';
|
|
import {FlatList, View, type ListRenderItemInfo, type NativeSyntheticEvent, type NativeScrollEvent} from 'react-native';
|
|
import Animated from 'react-native-reanimated';
|
|
|
|
import {GalleryInit} from '@context/gallery';
|
|
import {useTheme} from '@context/theme';
|
|
import {useChannelBookmarkFiles} from '@hooks/files';
|
|
import {usePreventDoubleTap} from '@hooks/utils';
|
|
import ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
|
import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery';
|
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|
|
|
import AddBookmark from './add_bookmark';
|
|
import ChannelBookmark from './channel_bookmark';
|
|
|
|
type Props = {
|
|
bookmarks: ChannelBookmarkModel[];
|
|
canAddBookmarks: boolean;
|
|
canDeleteBookmarks: boolean;
|
|
canDownloadFiles: boolean;
|
|
canEditBookmarks: boolean;
|
|
canUploadFiles: boolean;
|
|
channelId: string;
|
|
currentUserId: string;
|
|
enableSecureFilePreview: boolean;
|
|
publicLinkEnabled: boolean;
|
|
showInInfo: boolean;
|
|
separator?: boolean;
|
|
}
|
|
|
|
const GRADIENT_LOCATIONS: LinearGradientProps['locations'] = [0, 0.64, 1];
|
|
const SCROLL_OFFSET = 10;
|
|
|
|
const isCloseToBottom = ({layoutMeasurement, contentOffset, contentSize}: NativeScrollEvent) => {
|
|
return layoutMeasurement.width + contentOffset.x <= contentSize.width - SCROLL_OFFSET;
|
|
};
|
|
|
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|
container: {
|
|
flexDirection: 'row',
|
|
},
|
|
separator: {
|
|
height: 1,
|
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
|
marginTop: 8,
|
|
marginBottom: 24,
|
|
},
|
|
emptyItemSeparator: {
|
|
width: 20,
|
|
},
|
|
addContainer: {
|
|
width: 40,
|
|
alignContent: 'center',
|
|
},
|
|
gradient: {
|
|
height: 48,
|
|
width: 68,
|
|
position: 'absolute',
|
|
right: 0,
|
|
},
|
|
channelView: {
|
|
paddingHorizontal: 8,
|
|
},
|
|
}));
|
|
|
|
const ChannelBookmarks = ({
|
|
bookmarks, canAddBookmarks, canDeleteBookmarks, canDownloadFiles, canEditBookmarks, canUploadFiles,
|
|
channelId, currentUserId, enableSecureFilePreview, publicLinkEnabled, showInInfo, separator = true,
|
|
}: Props) => {
|
|
const galleryIdentifier = `${channelId}-bookmarks`;
|
|
const theme = useTheme();
|
|
const styles = getStyleSheet(theme);
|
|
const files = useChannelBookmarkFiles(bookmarks);
|
|
const [allowEndFade, setAllowEndFade] = useState(true);
|
|
|
|
const attachmentIndex = useCallback((fileId: string) => {
|
|
return files.findIndex((file) => file.id === fileId) || 0;
|
|
}, [files]);
|
|
|
|
const handlePreviewPress = usePreventDoubleTap(useCallback((idx: number) => {
|
|
if (files.length) {
|
|
const items = files.map((f) => fileToGalleryItem(f, f.user_id, undefined, 0, f.id));
|
|
openGalleryAtIndex(galleryIdentifier, idx, items);
|
|
}
|
|
}, [files, galleryIdentifier]));
|
|
|
|
const renderItem = useCallback(({item}: ListRenderItemInfo<ChannelBookmarkModel>) => {
|
|
return (
|
|
<ChannelBookmark
|
|
bookmark={item}
|
|
canDeleteBookmarks={canDeleteBookmarks}
|
|
canDownloadFiles={canDownloadFiles}
|
|
canEditBookmarks={canEditBookmarks}
|
|
enableSecureFilePreview={enableSecureFilePreview}
|
|
galleryIdentifier={galleryIdentifier}
|
|
index={item.fileId ? attachmentIndex(item.fileId) : undefined}
|
|
onPress={handlePreviewPress}
|
|
publicLinkEnabled={publicLinkEnabled}
|
|
/>
|
|
);
|
|
}, [
|
|
canDeleteBookmarks, canDownloadFiles, canEditBookmarks, enableSecureFilePreview,
|
|
galleryIdentifier, attachmentIndex, handlePreviewPress,
|
|
publicLinkEnabled,
|
|
]);
|
|
|
|
const renderItemSeparator = useCallback(() => (
|
|
<View style={styles.emptyItemSeparator}/>
|
|
), [styles.emptyItemSeparator]);
|
|
|
|
const onScrolled = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
setAllowEndFade(isCloseToBottom(e.nativeEvent));
|
|
}, []);
|
|
|
|
const gradientColors: LinearGradientProps['colors'] = useMemo(() => [
|
|
theme.centerChannelBg,
|
|
changeOpacity(theme.centerChannelBg, 0.6458),
|
|
changeOpacity(theme.centerChannelBg, 0),
|
|
], [theme]);
|
|
|
|
if (!bookmarks.length && showInInfo && canAddBookmarks) {
|
|
return (
|
|
<AddBookmark
|
|
bookmarksCount={0}
|
|
canUploadFiles={canUploadFiles}
|
|
channelId={channelId}
|
|
currentUserId={currentUserId}
|
|
showLarge={true}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (bookmarks.length) {
|
|
return (
|
|
<GalleryInit galleryIdentifier={galleryIdentifier}>
|
|
<Animated.View>
|
|
<Animated.View style={[styles.container, showInInfo ? undefined : styles.channelView]}>
|
|
<FlatList
|
|
bounces={true}
|
|
alwaysBounceHorizontal={false}
|
|
data={bookmarks}
|
|
horizontal={true}
|
|
renderItem={renderItem}
|
|
ItemSeparatorComponent={renderItemSeparator}
|
|
onScroll={onScrolled}
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={{height: 48, alignItems: 'center'}}
|
|
/>
|
|
{canAddBookmarks &&
|
|
<View style={styles.addContainer}>
|
|
{allowEndFade &&
|
|
<LinearGradient
|
|
locations={GRADIENT_LOCATIONS}
|
|
colors={gradientColors}
|
|
style={styles.gradient}
|
|
pointerEvents={'none'}
|
|
/>
|
|
}
|
|
<AddBookmark
|
|
bookmarksCount={bookmarks.length}
|
|
canUploadFiles={canUploadFiles}
|
|
channelId={channelId}
|
|
currentUserId={currentUserId}
|
|
showLarge={false}
|
|
/>
|
|
</View>
|
|
}
|
|
</Animated.View>
|
|
{separator &&
|
|
<View style={styles.separator}/>
|
|
}
|
|
</Animated.View>
|
|
</GalleryInit>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
export default ChannelBookmarks;
|