mattermost-mobile/app/components/channel_bookmarks/channel_bookmarks.tsx
Elias Nahum 44eb76bed7
feat(iOS): Add Microsoft Intune MAM integration with multi-server support (#9312)
* 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>
2025-12-10 13:07:28 +02:00

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;