mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-10 05:00:13 +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>
219 lines
7.5 KiB
TypeScript
219 lines
7.5 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {AppState, type AppStateStatus} from 'react-native';
|
|
|
|
import {updateDraftFile} from '@actions/local/draft';
|
|
import {uploadFile} from '@actions/remote/file';
|
|
import {ServerErrors} from '@constants';
|
|
import {PROGRESS_TIME_TO_STORE} from '@constants/files';
|
|
import RenderPermissionsStore from '@store/render_permissions_store';
|
|
import {getFullErrorMessage} from '@utils/errors';
|
|
|
|
import type {ClientResponse, ClientResponseError} from '@mattermost/react-native-network-client';
|
|
|
|
type FileHandler = {
|
|
[clientId: string]: {
|
|
cancel?: () => void;
|
|
fileInfo: FileInfo;
|
|
serverUrl: string;
|
|
channelId: string;
|
|
rootId: string;
|
|
lastTimeStored: number;
|
|
onError: Array<(msg: string, name?: string) => void>;
|
|
onProgress: Array<(p: number, b: number) => void>;
|
|
isEditPost?: boolean;
|
|
updateFileCallback?: (fileInfo: FileInfo) => void;
|
|
};
|
|
}
|
|
|
|
class DraftEditPostUploadManagerSingleton {
|
|
private handlers: FileHandler = {};
|
|
private previousAppState: AppStateStatus;
|
|
|
|
constructor() {
|
|
this.previousAppState = AppState.currentState;
|
|
AppState.addEventListener('change', this.onAppStateChange);
|
|
}
|
|
|
|
public prepareUpload = (
|
|
serverUrl: string,
|
|
file: FileInfo,
|
|
channelId: string,
|
|
rootId: string,
|
|
skipBytes = 0,
|
|
isEditPost = false,
|
|
updateFileCallback?: (fileInfo: FileInfo) => void,
|
|
) => {
|
|
this.handlers[file.clientId!] = {
|
|
fileInfo: file,
|
|
serverUrl,
|
|
channelId,
|
|
rootId,
|
|
lastTimeStored: 0,
|
|
onError: [],
|
|
onProgress: [],
|
|
isEditPost,
|
|
updateFileCallback,
|
|
};
|
|
|
|
const onProgress = (progress: number, bytesRead?: number | null | undefined) => {
|
|
this.handleProgress(file.clientId!, progress, bytesRead || 0);
|
|
};
|
|
|
|
const onComplete = (response: ClientResponse) => {
|
|
this.handleComplete(response, file.clientId!);
|
|
};
|
|
|
|
const onError = (response: ClientResponseError) => {
|
|
const message = response.message || 'Unknown error';
|
|
const errorName = (response as ClientResponseError & {name?: string}).name;
|
|
this.handleError(message, file.clientId!, errorName);
|
|
};
|
|
|
|
const {error, cancel} = uploadFile(serverUrl, file, channelId, onProgress, onComplete, onError, skipBytes);
|
|
if (error) {
|
|
this.handleError(getFullErrorMessage(error), file.clientId!);
|
|
return;
|
|
}
|
|
this.handlers[file.clientId!].cancel = cancel;
|
|
};
|
|
|
|
public cancel = (clientId: string) => {
|
|
if (this.handlers[clientId]?.cancel) {
|
|
this.handlers[clientId].cancel?.();
|
|
delete this.handlers[clientId];
|
|
}
|
|
};
|
|
|
|
public isUploading = (clientId: string) => {
|
|
return Boolean(this.handlers[clientId]);
|
|
};
|
|
|
|
public registerProgressHandler = (clientId: string, callback: (progress: number, bytes: number) => void) => {
|
|
if (!this.handlers[clientId]) {
|
|
return undefined;
|
|
}
|
|
|
|
this.handlers[clientId].onProgress.push(callback);
|
|
return () => {
|
|
if (!this.handlers[clientId]) {
|
|
return;
|
|
}
|
|
|
|
this.handlers[clientId].onProgress = this.handlers[clientId].onProgress.filter((v) => v !== callback);
|
|
};
|
|
};
|
|
|
|
public registerErrorHandler = (clientId: string, callback: (errMessage: string, errName?: string) => void) => {
|
|
if (!this.handlers[clientId]) {
|
|
return undefined;
|
|
}
|
|
|
|
this.handlers[clientId].onError.push(callback);
|
|
return () => {
|
|
if (!this.handlers[clientId]) {
|
|
return;
|
|
}
|
|
|
|
this.handlers[clientId].onError = this.handlers[clientId].onError.filter((v) => v !== callback);
|
|
};
|
|
};
|
|
|
|
private handleProgress = (clientId: string, progress: number, bytes: number) => {
|
|
const h = this.handlers[clientId];
|
|
if (!h) {
|
|
return;
|
|
}
|
|
|
|
h.fileInfo.bytesRead = bytes;
|
|
|
|
h.onProgress.forEach((c) => c(progress, bytes));
|
|
if (AppState.currentState !== 'active' && h.lastTimeStored + PROGRESS_TIME_TO_STORE < Date.now()) {
|
|
this.handleUpdateDraftFile(h, this.handlers[clientId].fileInfo, h.isEditPost || false);
|
|
h.lastTimeStored = Date.now();
|
|
}
|
|
};
|
|
|
|
private handleComplete = async (response: ClientResponse, clientId: string) => {
|
|
const h = this.handlers[clientId];
|
|
if (!h) {
|
|
return;
|
|
}
|
|
if (response.code !== 201) {
|
|
// The network client resolves HTTP errors rather than rejecting them, so the server's error
|
|
// id is only available here; it lets callers recognize specific refusals.
|
|
const errorId = response.data?.id as string | undefined;
|
|
if (errorId === ServerErrors.UPLOAD_DENIED_BY_POLICY_ERROR) {
|
|
// The stored decision allowed an upload the server refused, so it is out of date.
|
|
RenderPermissionsStore.expireEntry(h.serverUrl, h.channelId);
|
|
}
|
|
this.handleError((response.data?.message as string | undefined) || 'Failed to upload the file: unknown error', clientId, errorId);
|
|
return;
|
|
}
|
|
if (!response.data) {
|
|
this.handleError('Failed to upload the file: no data received', clientId);
|
|
return;
|
|
}
|
|
const data = response.data.file_infos as FileInfo[] | undefined;
|
|
if (!data?.length) {
|
|
this.handleError('Failed to upload the file: no data received', clientId);
|
|
return;
|
|
}
|
|
|
|
delete this.handlers[clientId];
|
|
|
|
const fileInfo = data[0];
|
|
fileInfo.clientId = h.fileInfo.clientId;
|
|
fileInfo.localPath = h.fileInfo.localPath;
|
|
|
|
await this.handleUpdateDraftFile(h, fileInfo, h.isEditPost || false);
|
|
};
|
|
|
|
private handleError = async (errorMessage: string, clientId: string, errorName?: string) => {
|
|
const h = this.handlers[clientId];
|
|
if (!h) {
|
|
return;
|
|
}
|
|
|
|
delete this.handlers[clientId];
|
|
|
|
h.onError.forEach((c) => c(errorMessage, errorName));
|
|
|
|
const fileInfo = {...h.fileInfo};
|
|
fileInfo.failed = true;
|
|
await this.handleUpdateDraftFile(h, fileInfo, h.isEditPost || false);
|
|
};
|
|
|
|
private handleUpdateDraftFile = async (handler: FileHandler[string], fileInfo: FileInfo, isEditPost: boolean) => {
|
|
if (isEditPost && handler.updateFileCallback) {
|
|
handler.updateFileCallback(fileInfo);
|
|
} else {
|
|
await updateDraftFile(handler.serverUrl, handler.channelId, handler.rootId, fileInfo);
|
|
}
|
|
};
|
|
|
|
private onAppStateChange = async (appState: AppStateStatus) => {
|
|
if (appState !== 'active' && this.previousAppState === 'active') {
|
|
await this.storeProgress();
|
|
}
|
|
|
|
this.previousAppState = appState;
|
|
};
|
|
|
|
private storeProgress = async () => {
|
|
for (const h of Object.values(this.handlers)) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.handleUpdateDraftFile(h, h.fileInfo, h.isEditPost || false);
|
|
h.lastTimeStored = Date.now();
|
|
}
|
|
};
|
|
}
|
|
|
|
const DraftEditPostUploadManager = new DraftEditPostUploadManagerSingleton();
|
|
export default DraftEditPostUploadManager;
|
|
|
|
export const exportedForTesting = {
|
|
DraftEditPostUploadManagerSingleton,
|
|
};
|