Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions client/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2514,13 +2514,10 @@
"impossibleToOpen": "Cannot edit this file",
"genericError": "Failed to open the file",
"unsupportedFileType": "File not supported by Parsec",
"editionNotAvailable": "Document editing is unavailable",
"tooLongToOpen": "The file is taking too long to open"
"editionNotAvailable": "Document editing is unavailable"
},
"informationEditDownload": "If you want to edit this file, you can download it and open it locally on your device.",
"informationPreviewDownload": "If you want to preview this file, you can download it and open it locally on your device.",
"tooLongToOpenOnWeb": "You can try downloading it and opening with default app (right click on file > 'Download').",
"tooLongToOpenOnDesktop": "You can try opening it with default app (right click on file > 'Open with default app').",
"unknownFileExtension": "This file type extension could not be identified. Please check that it's correctly written (.txt, .pdf, .doc, etc.).",
"noContentFileType": "Could not open this file in the Parsec editor as its type could not be identified.",
"noFolderPreview": "Cannot preview folders.",
Expand Down Expand Up @@ -2579,9 +2576,12 @@
"globalTitle": "Cannot open file",
"errors": {
"titles": {
"networkOffline": "Network connection lost"
"networkOffline": "Network connection lost",
"tooLongToOpen": "The file seems to be taking too long to open"
},
"networkOfflineMessage": "Your network connection has been lost. Please check your internet connection and try again. Any unsaved changes may be lost.",
"tooLongToOpenOnWeb": "You can try downloading it and opening with default app (right click on file > 'Download').",
"tooLongToOpenOnDesktop": "You can try opening it with default app (right click on file > 'Open with default app').",
"contentInfoMissing": "The file content information is empty or missing.",
"fileExtensionMissing": "The file type (extension) is missing or empty.",
"fileNameMissing": "The file name is empty or missing.",
Expand All @@ -2591,12 +2591,15 @@
"advices": {
"title": "Some tips to help resolve this issue",
"advice1": "Check that the file can be opened from an external application.",
"advice2": "Re-import the file (check versions).",
"advice3": "If the file used to open properly, try restoring a previous version (through file history)"
"advice2": "If the file used to open properly, try restoring a previous version (through file history)"
},
"actions": {
"backToFiles": "Back to files",
"retry": "Reload page"
"retry": "Reload page",
"openWithDefaultApp": "Open with default app",
"downloadFile": "Download file",
"wait": "Wait",
"close": "Close"
},
"saving": {
"unsaved": "Changes unsaved",
Expand Down
19 changes: 11 additions & 8 deletions client/src/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -2515,13 +2515,10 @@
"impossibleToOpen": "Impossible d'éditer ce fichier",
"genericError": "Impossible d'ouvrir ce fichier",
"unsupportedFileType": "Type de fichier non pris en charge par Parsec",
"editionNotAvailable": "L'édition de documents n'est pas disponible",
"tooLongToOpen": "Le fichier met trop de temps à s'ouvrir"
"editionNotAvailable": "L'édition de documents n'est pas disponible"
},
"informationEditDownload": "Si vous souhaitez modifier ce fichier, vous pouvez le télécharger et l'ouvrir localement sur votre appareil.",
"informationPreviewDownload": "Si vous souhaitez prévisualiser ce fichier, vous pouvez le faire dans Parsec ou le télécharger pour l'ouvrir localement sur votre appareil.",
"tooLongToOpenOnWeb": "Vous pouvez essayer de le télécharger et de l'ouvrir avec l'application par défaut (clic droit sur le fichier > 'Télécharger').",
"tooLongToOpenOnDesktop": "Vous pouvez essayer de l'ouvrir avec l'application par défaut (clic droit sur le fichier > 'Ouvrir avec l'app par défaut').",
"unknownFileExtension": "L'extension de ce fichier n'a pas pu être reconnue. Veuillez vérifier qu'elle est correctement écrite (.txt, .pdf, .doc, etc.).",
"noContentFileType": "Impossible d'ouvrir ce fichier, son type n'ayant pas pu être reconnu dans l'éditeur de Parsec.",
"noFolderPreview": "Impossible d'afficher l'aperçu d'un dossier.",
Expand Down Expand Up @@ -2580,9 +2577,12 @@
"globalTitle": "Impossible d'ouvrir le fichier",
"errors": {
"titles": {
"networkOffline": "Connexion réseau perdue"
"networkOffline": "Connexion réseau perdue",
"tooLongToOpen": "Le fichier semble mettre trop de temps à s'ouvrir"
},
"networkOfflineMessage": "Votre connexion réseau a été perdue. Veuillez vérifier votre connexion internet et réessayer. Les modifications non enregistrées peuvent être perdues.",
"tooLongToOpenOnWeb": "Vous pouvez essayer de le télécharger et de l'ouvrir avec l'application par défaut (clic droit sur le fichier > 'Télécharger').",
"tooLongToOpenOnDesktop": "Vous pouvez essayer de l'ouvrir avec l'application par défaut (clic droit sur le fichier > 'Ouvrir avec l'app par défaut').",
"contentInfoMissing": "Les informations sur le contenu du fichier sont vides ou manquantes.",
"fileExtensionMissing": "Le type de fichier (extension) est manquant ou vide.",
"fileNameMissing": "Le nom du fichier est vide ou manquant.",
Expand All @@ -2592,12 +2592,15 @@
"advices": {
"title": "Quelques conseils pour résoudre ce problème",
"advice1": "Vérifier que le fichier s'ouvre correctement depuis un logiciel externe.",
"advice2": "Ré-importer le fichier (vérifier les versions).",
"advice3": "Si le fichier s'ouvrait avant, essayer de restaurer une version antérieure (via l'historique du fichier)"
"advice2": "Si le fichier s'ouvrait avant, essayer de restaurer une version antérieure (via l'historique du fichier)"
},
"actions": {
"backToFiles": "Retour aux fichiers",
"retry": "Recharger la page"
"retry": "Recharger la page",
"openWithDefaultApp": "Ouvrir avec l'app par défaut",
"downloadFile": "Télécharger le fichier",
"wait": "Attendre",
"close": "Fermer"
},
"saving": {
"unsaved": "Modifications non sauvegardées",
Expand Down
37 changes: 2 additions & 35 deletions client/src/services/cryptpad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,44 +173,11 @@ export class Cryptpad {
throw new CryptpadOpenError(CryptpadErrorCode.DocumentTypeNotEnabled, config.documentType);
}

// Set up a loading timeout (30 seconds) to detect stuck/corrupted files
const LOADING_TIMEOUT_MS = 30000;
let loadingTimeoutId: any = undefined;

// Create a promise that will be resolved when CryptPad successfully loads
let resolveLoading!: () => void;
const loadingComplete = new Promise<void>((resolve) => {
resolveLoading = resolve;
});

const timeoutPromise = new Promise<void>((_, reject) => {
loadingTimeoutId = window.setTimeout(() => {
const message = 'CryptPad loading timeout - the file may be corrupted, too large, or the server is not responding.';
window.electronAPI.log('warn', message);
reject(new CryptpadOpenError(CryptpadErrorCode.LoadingTimeout, config.documentType, message));
}, LOADING_TIMEOUT_MS);
});
// Wrap the original onReady callback to signal successful loading
const originalOnReady = config.events.onReady;
config.events.onReady = () => {
resolveLoading();
if (originalOnReady) originalOnReady();
};

// Store config globally so CryptPad customization can access the onError callback
(window as any).cryptpadConfig = config;

try {
// Start CryptPad API (doesn't wait for loading to complete)
void (window as any).CryptPadAPI(this.containerElement.id, { ...config });

// Race between successful loading (onReady callback) and timeout
await Promise.race([loadingComplete, timeoutPromise]);
} finally {
window.clearTimeout(loadingTimeoutId);
// Clean up global config reference
delete (window as any).cryptpadConfig;
}
// Start CryptPad API (doesn't wait for loading to complete)
void (window as any).CryptPadAPI(this.containerElement.id, { ...config });
}
}

Expand Down
174 changes: 174 additions & 0 deletions client/src/views/files/handler/editor/EditorIssueModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<!-- Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS -->

<template>
<ms-modal
:title="modalConfig.title"
:close-button="{ visible: false }"
:cancel-button="modalConfig.cancelButton"
:confirm-button="modalConfig.confirmButton"
>
<div class="editor-error">
<ms-report-text
v-if="modalConfig.message"
:theme="modalConfig.theme"
>
{{ $msTranslate(modalConfig.message) }}
</ms-report-text>
<div
v-if="status === EditorIssueStatus.LoadingTimeout"
class="editor-error-advices"
>
<ion-text class="editor-error-advices__title title-h5">{{ $msTranslate('fileEditors.advices.title') }}</ion-text>
<ion-list class="editor-error-advices-list ion-no-padding">
<ion-item class="editor-error-advices-list__item ion-no-padding body">
<ion-icon
class="item-icon"
:icon="checkmarkCircle"
/>
{{ $msTranslate('fileEditors.advices.advice1') }}
</ion-item>
<ion-item class="editor-error-advices-list__item ion-no-padding body">
<ion-icon
class="item-icon"
:icon="checkmarkCircle"
/>
{{ $msTranslate('fileEditors.advices.advice2') }}
</ion-item>
</ion-list>
</div>
</div>
</ms-modal>
</template>

<script setup lang="ts">
import { isWeb } from '@/parsec';
import { EditorButtonAction, EditorErrorMessage, EditorErrorTitle, EditorIssueStatus } from '@/views/files/handler/editor/types';
import { IonIcon, IonItem, IonList, IonText, modalController } from '@ionic/vue';
import { checkmarkCircle } from 'ionicons/icons';
import { MsModal, MsModalResult, MsReportText, MsReportTheme, Translatable } from 'megashark-lib';
import { computed, Ref } from 'vue';

const props = defineProps<{
status: EditorIssueStatus;
fileLoaded?: Ref<boolean>;
}>();

interface ModalConfig {
title: Translatable;
message?: Translatable;
theme: MsReportTheme;
confirmButton: {
label: Translatable;
disabled: boolean;
onClick: () => Promise<boolean>;
};
cancelButton?: {
label: Translatable;
disabled: boolean;
};
}

const modalConfig = computed<ModalConfig>(() => {
let title: Translatable;
let message: Translatable | undefined;
let theme: MsReportTheme;
let confirmButtonLabel: Translatable;
let cancelButton: { label: Translatable; disabled: boolean } | undefined;

switch (props.status) {
case EditorIssueStatus.EditionNotAvailable:
title = EditorErrorTitle.EditionNotAvailable;
message = EditorErrorMessage.EditableOnlyOnSystem;
theme = MsReportTheme.Info;
confirmButtonLabel = EditorButtonAction.BackToFiles;
break;
case EditorIssueStatus.LoadingTimeout:
title = EditorErrorTitle.TooLongToOpen;
message = isWeb() ? EditorErrorMessage.TooLongToOpenOnWeb : EditorErrorMessage.TooLongToOpenOnDesktop;
theme = MsReportTheme.Warning;
confirmButtonLabel = props.fileLoaded?.value ? EditorButtonAction.Close : EditorButtonAction.Wait;
if (!props.fileLoaded?.value) {
cancelButton = {
label: EditorButtonAction.BackToFiles,
disabled: false,
};
}
break;
case EditorIssueStatus.NetworkOffline:
title = EditorErrorTitle.NetworkOffline;
message = EditorErrorMessage.NetworkOffline;
theme = MsReportTheme.Warning;
confirmButtonLabel = EditorButtonAction.Close;
break;
case EditorIssueStatus.UnsupportedFileType:
title = EditorErrorTitle.UnsupportedFileType;
message = EditorErrorMessage.EditableOnlyOnSystem;
theme = MsReportTheme.Info;
confirmButtonLabel = EditorButtonAction.BackToFiles;
break;
default:
title = EditorErrorTitle.GenericError;
theme = MsReportTheme.Warning;
confirmButtonLabel = EditorButtonAction.BackToFiles;
break;
}

return {
title,
message,
theme,
confirmButton: {
label: confirmButtonLabel,
disabled: false,
onClick: async (): Promise<boolean> => {
return modalController.dismiss(true, MsModalResult.Confirm);
},
},
cancelButton,
};
});
</script>

<style scoped lang="scss">
.editor-error {
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;

&-advices {
border-top: 1px solid var(--parsec-color-light-secondary-disabled);
padding-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;

&__title {
color: var(--parsec-color-light-secondary-text);
}

&-list {
padding-left: 0.5rem;
list-style-type: circle;
background: none;

&__item {
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--parsec-color-light-secondary-soft-text);
--background: none;
font-size: 0.9375rem;

.item-icon {
color: var(--parsec-color-light-secondary-grey);
flex-shrink: 0;
margin-right: 0.5rem;
font-size: 1rem;
}
}
}
}
}
</style>
Loading
Loading