mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-04 09:58:57 +00:00
* MM-68219 - Add redacted files placeholder and related functionality * add feature flag control and e2e tests * adjust placeholder design, polish code, fix permalink preview issue * polish code and add missing test * use import path with right path * gate post refetch behind the feature flag, refetch the open thread and avoid observeCount * restore filesInfo for the permalink files prop added upstream * fix stale redaction on background servers, consume the flag on every fetch path and share the viewport emitter * replace the in-memory redaction flag with a persisted per-post epoch so ABAC file decisions survive restarts and stale posts stay hidden * raise the redaction epoch on every resync, capture the epoch counter, only invalidate channels on role changes, and store the newer epoch when a re-verified post is otherwise unchanged * store superseded responses unverified instead of dropping them, keep the since-fetch when re-verifying, verify permalink embeds against the linked channel, and capture the websocket epoch on arrival * nvalidate file decisions when a user attribute field changes or all its values are cleared * batch channel invalidations, close file viewers only when affected, reset the epoch on unstamped redaction changes, and only re-verify the newest page's attachment posts * re-check older channel posts a page at a time instead of one request per post * skip the unstamped epoch lookup for posts with no files, redacted count or permalink * keep the attribute-view retry after later invalidations, retry linked posts unless refused for the current epoch, and stop older payloads from rolling back edits via the redaction check * MM-70905 - disable and grey out the attachment button when an ABAC pe… (#10195) * MM-70905 - disable and grey out the attachment button when an ABAC permission policy denies uploads in the channel * only back off render permission fetches when the server request fails, and log other errors without retrying * remove the disableServer helper and have removeServer do both steps * split defaultAllowed into allowWhenUnenforced and allowWhenNotEvaluated, and treat render permissions as unenforced on servers without the decisions API * MM-69627: Add playbook task requirements support on mobile (#10035) * MM-69627: Add playbook task requirements support on mobile Allow filling required task fields when checking off checklist items during a run, gated by the same Playbooks beta setting used on web. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Address PR review feedback for task requirements Propagate settings write failures, harden fill-requirements save/title handling, and add missing unit coverage. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Fix TypeScript errors in new unit tests Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Use defineMessages for fill requirements titles Keep i18n extraction able to find the header titles after moving them to the route. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Address review feedback for task requirements Update checklist items locally when saving requirement values instead of refetching the run, add missing query/navigation tests, and apply small callback/reduce cleanups from review. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Fix task requirements review follow-ups Propagate local checklist write failures, clear a stale task-requirements flag when playbooks is unsupported, and cap fill values at the desktop/ server max length. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69627: Move requirement field into its own component Keep one component per file on the fill-requirements screen. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> * MM-69122: Add file element support to interactive dialogs (#9898) * Implement Interactive Dialog - file upload * update demo plugin version for e2etests * fix and add e2etests * review fixes * Fix version bump checkout when a tag shares the branch name (#10229) * Fix Android crashes caused by animated custom emoji (#10030) * Route GIF decoding through Glide - Keep APNG4Android for APNG, animated WebP, and animated AVIF support. - Prepend Glide's GIF decoders to avoid APNG4Android's native GIF overflow. * Clear Expo image resources with Glide targets - Detach drawables when Glide clears their custom target. - Prevent recycled GIF frame resources from remaining attached to Expo image views. * Preserve foreground image during GIF target recycling --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com> Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com> * detox - fix clobbering of feature flags that cause failures in other suites. Also small fix of a incorrectly set test-id --------- Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Nuno Simões <nuno.simoes@mattermost.com> Co-authored-by: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Co-authored-by: Nate Schlossberg <nate.schlossberg@mattermost.com>
197 lines
7.1 KiB
TypeScript
197 lines
7.1 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {
|
|
SecurePdfViewer,
|
|
type OnLinkPressedEvent,
|
|
type OnLoadErrorEvent,
|
|
type OnPasswordFailedEvent,
|
|
type OnPasswordLimitReachedEvent,
|
|
type OnPasswordRequiredEvent,
|
|
} from '@mattermost/secure-pdf-viewer';
|
|
import {useNavigation} from 'expo-router';
|
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
import {useIntl} from 'react-intl';
|
|
import {Alert, DeviceEventEmitter, View} from 'react-native';
|
|
|
|
import {setFileAsBlocked} from '@actions/local/file';
|
|
import {Events, Screens} from '@constants';
|
|
import {useServerUrl} from '@context/server';
|
|
import {useTheme} from '@context/theme';
|
|
import DatabaseManager from '@database/manager';
|
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
|
import useDidMount from '@hooks/did_mount';
|
|
import {getFileById} from '@queries/servers/file';
|
|
import {navigateBack} from '@screens/navigation';
|
|
import CallbackStore from '@store/callback_store';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import {deleteFile} from '@utils/file';
|
|
import {logError} from '@utils/log';
|
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|
import {openLink} from '@utils/url/links';
|
|
|
|
import PdfLoadError from './error';
|
|
import PdfPassword, {type PasswordRef} from './password';
|
|
|
|
type Props = {
|
|
allowPdfLinkNavigation: boolean;
|
|
fileId: string;
|
|
filePath: string;
|
|
siteURL: string;
|
|
}
|
|
|
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|
flex: {
|
|
flex: 1,
|
|
},
|
|
pdfView: {
|
|
flex: 1,
|
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
|
},
|
|
}));
|
|
|
|
const dismiss = () => {
|
|
const onDismiss = CallbackStore.getCallback<() => void>();
|
|
onDismiss?.();
|
|
CallbackStore.removeCallback();
|
|
};
|
|
|
|
const PdfViewer = ({allowPdfLinkNavigation, fileId, filePath, siteURL}: Props) => {
|
|
const navigation = useNavigation();
|
|
const theme = useTheme();
|
|
const serverUrl = useServerUrl();
|
|
const intl = useIntl();
|
|
const styles = getStyleSheet(theme);
|
|
const [password, setPassword] = useState<string | undefined>(undefined);
|
|
const passwordRef = useRef<PasswordRef>(null);
|
|
const [maxAttempts, setMaxAttempts] = useState<number | undefined>(undefined);
|
|
const [remainingAttempts, setRemainingAttempts] = useState<number | undefined>(undefined);
|
|
const [navBarVisible, setNavBarVisible] = useState(true);
|
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
|
const [promptForPassword, setPromptForPassword] = useState(false);
|
|
|
|
const toggleNavbar = useCallback((visible: boolean) => {
|
|
navigation.setOptions({
|
|
headerShown: visible,
|
|
});
|
|
}, [navigation]);
|
|
|
|
const onClose = useCallback(() => {
|
|
return navigateBack();
|
|
}, []);
|
|
|
|
const onLinkPressed = useCallback((event: OnLinkPressedEvent) => {
|
|
openLink(event.nativeEvent.url, serverUrl, siteURL, intl);
|
|
}, [intl, serverUrl, siteURL]);
|
|
|
|
const onLinkPressedDisabled = useCallback(() => {
|
|
logError('Error opening link');
|
|
Alert.alert(
|
|
intl.formatMessage({id: 'mobile.pdf_viewer.link_disabled_title', defaultMessage: 'Link Disabled'}),
|
|
intl.formatMessage({id: 'mobile.pdf_viewer.link_disabled_detail', defaultMessage: "Opening links inside this PDF is not allowed by your organization's security settings."}),
|
|
);
|
|
}, [intl]);
|
|
|
|
const onLoad = useCallback(() => {
|
|
setMaxAttempts(undefined);
|
|
setRemainingAttempts(undefined);
|
|
}, []);
|
|
|
|
const onLoadError = useCallback((event: OnLoadErrorEvent) => {
|
|
logError('Error loading PDF', event.nativeEvent.message);
|
|
setErrorMessage(event.nativeEvent.message);
|
|
deleteFile(filePath);
|
|
}, [filePath]);
|
|
|
|
const onPasswordFailed = useCallback((event: OnPasswordFailedEvent) => {
|
|
setRemainingAttempts(event.nativeEvent.remainingAttempts);
|
|
setPassword(undefined);
|
|
passwordRef.current?.clear();
|
|
}, []);
|
|
|
|
const onPasswordFailureLimitReached = useCallback((event: OnPasswordLimitReachedEvent) => {
|
|
passwordRef.current?.clear();
|
|
setMaxAttempts(event.nativeEvent.maxAttempts);
|
|
setRemainingAttempts(0);
|
|
setFileAsBlocked(serverUrl, fileId);
|
|
}, [serverUrl, fileId]);
|
|
|
|
const onPasswordRequired = useCallback((event: OnPasswordRequiredEvent) => {
|
|
setMaxAttempts(event.nativeEvent.maxAttempts);
|
|
setRemainingAttempts(event.nativeEvent.remainingAttempts);
|
|
}, []);
|
|
|
|
const onTap = useCallback(() => {
|
|
setNavBarVisible((prev) => !prev);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
toggleNavbar(navBarVisible);
|
|
}, [navBarVisible, toggleNavbar]);
|
|
|
|
useAndroidHardwareBackHandler(Screens.PDF_VIEWER, onClose);
|
|
|
|
useEffect(() => setPromptForPassword(maxAttempts !== undefined && maxAttempts > 0), [maxAttempts]);
|
|
|
|
useDidMount(() => {
|
|
return dismiss;
|
|
});
|
|
|
|
useDidMount(() => {
|
|
let active = true;
|
|
const previousFileViewerPostId = EphemeralStore.getCurrentFileViewerPostId();
|
|
(async () => {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const file = await getFileById(database, fileId);
|
|
if (active && file?.postId) {
|
|
EphemeralStore.setCurrentFileViewerPostId(file.postId);
|
|
}
|
|
} catch {
|
|
// file not in DB — no protection needed
|
|
}
|
|
})();
|
|
return () => {
|
|
active = false;
|
|
EphemeralStore.setCurrentFileViewerPostId(previousFileViewerPostId);
|
|
};
|
|
});
|
|
|
|
// An ABAC change can revoke access to this document while it is open, and the viewer was handed
|
|
// its file by value, so no database change can reach it. The gallery listens for the same event.
|
|
useEffect(() => {
|
|
const subscription = DeviceEventEmitter.addListener(Events.CLOSE_GALLERY, onClose);
|
|
return () => subscription.remove();
|
|
}, [onClose]);
|
|
|
|
return (
|
|
<View style={styles.flex}>
|
|
{Boolean(errorMessage) && <PdfLoadError message={errorMessage}/>}
|
|
{promptForPassword &&
|
|
<PdfPassword
|
|
maxAttempts={maxAttempts}
|
|
ref={passwordRef}
|
|
remainingAttempts={remainingAttempts}
|
|
setPassword={setPassword}
|
|
/>
|
|
}
|
|
<SecurePdfViewer
|
|
allowLinks={allowPdfLinkNavigation}
|
|
onLinkPressed={onLinkPressed}
|
|
onLinkPressedDisabled={onLinkPressedDisabled}
|
|
onLoad={onLoad}
|
|
onLoadError={onLoadError}
|
|
onPasswordFailed={onPasswordFailed}
|
|
onPasswordFailureLimitReached={onPasswordFailureLimitReached}
|
|
onPasswordRequired={onPasswordRequired}
|
|
onTap={onTap}
|
|
password={password}
|
|
source={filePath}
|
|
style={styles.pdfView}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default PdfViewer;
|