Skip to content

Commit b47c9b1

Browse files
committed
feat(editor): add timeout modal for file loading issues and enhance error handling
1 parent c358e93 commit b47c9b1

File tree

6 files changed

+360
-128
lines changed

6 files changed

+360
-128
lines changed

client/src/locales/en-US.json

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2514,13 +2514,10 @@
25142514
"impossibleToOpen": "Cannot edit this file",
25152515
"genericError": "Failed to open the file",
25162516
"unsupportedFileType": "File not supported by Parsec",
2517-
"editionNotAvailable": "Document editing is unavailable",
2518-
"tooLongToOpen": "The file is taking too long to open"
2517+
"editionNotAvailable": "Document editing is unavailable"
25192518
},
25202519
"informationEditDownload": "If you want to edit this file, you can download it and open it locally on your device.",
25212520
"informationPreviewDownload": "If you want to preview this file, you can download it and open it locally on your device.",
2522-
"tooLongToOpenOnWeb": "You can try downloading it and opening with default app (right click on file > 'Download').",
2523-
"tooLongToOpenOnDesktop": "You can try opening it with default app (right click on file > 'Open with default app').",
25242521
"unknownFileExtension": "This file type extension could not be identified. Please check that it's correctly written (.txt, .pdf, .doc, etc.).",
25252522
"noContentFileType": "Could not open this file in the Parsec editor as its type could not be identified.",
25262523
"noFolderPreview": "Cannot preview folders.",
@@ -2579,9 +2576,12 @@
25792576
"globalTitle": "Cannot open file",
25802577
"errors": {
25812578
"titles": {
2582-
"networkOffline": "Network connection lost"
2579+
"networkOffline": "Network connection lost",
2580+
"tooLongToOpen": "The file seems to be taking too long to open"
25832581
},
25842582
"networkOfflineMessage": "Your network connection has been lost. Please check your internet connection and try again. Any unsaved changes may be lost.",
2583+
"tooLongToOpenOnWeb": "You can try downloading it and opening with default app (right click on file > 'Download').",
2584+
"tooLongToOpenOnDesktop": "You can try opening it with default app (right click on file > 'Open with default app').",
25852585
"contentInfoMissing": "The file content information is empty or missing.",
25862586
"fileExtensionMissing": "The file type (extension) is missing or empty.",
25872587
"fileNameMissing": "The file name is empty or missing.",
@@ -2591,12 +2591,15 @@
25912591
"advices": {
25922592
"title": "Some tips to help resolve this issue",
25932593
"advice1": "Check that the file can be opened from an external application.",
2594-
"advice2": "Re-import the file (check versions).",
2595-
"advice3": "If the file used to open properly, try restoring a previous version (through file history)"
2594+
"advice2": "If the file used to open properly, try restoring a previous version (through file history)"
25962595
},
25972596
"actions": {
25982597
"backToFiles": "Back to files",
2599-
"retry": "Reload page"
2598+
"retry": "Reload page",
2599+
"openWithDefaultApp": "Open with default app",
2600+
"downloadFile": "Download file",
2601+
"wait": "Wait",
2602+
"close": "Close"
26002603
},
26012604
"saving": {
26022605
"unsaved": "Changes unsaved",

client/src/locales/fr-FR.json

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,13 +2515,10 @@
25152515
"impossibleToOpen": "Impossible d'éditer ce fichier",
25162516
"genericError": "Impossible d'ouvrir ce fichier",
25172517
"unsupportedFileType": "Type de fichier non pris en charge par Parsec",
2518-
"editionNotAvailable": "L'édition de documents n'est pas disponible",
2519-
"tooLongToOpen": "Le fichier met trop de temps à s'ouvrir"
2518+
"editionNotAvailable": "L'édition de documents n'est pas disponible"
25202519
},
25212520
"informationEditDownload": "Si vous souhaitez modifier ce fichier, vous pouvez le télécharger et l'ouvrir localement sur votre appareil.",
25222521
"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.",
2523-
"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').",
2524-
"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').",
25252522
"unknownFileExtension": "L'extension de ce fichier n'a pas pu être reconnue. Veuillez vérifier qu'elle est correctement écrite (.txt, .pdf, .doc, etc.).",
25262523
"noContentFileType": "Impossible d'ouvrir ce fichier, son type n'ayant pas pu être reconnu dans l'éditeur de Parsec.",
25272524
"noFolderPreview": "Impossible d'afficher l'aperçu d'un dossier.",
@@ -2580,9 +2577,12 @@
25802577
"globalTitle": "Impossible d'ouvrir le fichier",
25812578
"errors": {
25822579
"titles": {
2583-
"networkOffline": "Connexion réseau perdue"
2580+
"networkOffline": "Connexion réseau perdue",
2581+
"tooLongToOpen": "Le fichier semble mettre trop de temps à s'ouvrir"
25842582
},
25852583
"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.",
2584+
"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').",
2585+
"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').",
25862586
"contentInfoMissing": "Les informations sur le contenu du fichier sont vides ou manquantes.",
25872587
"fileExtensionMissing": "Le type de fichier (extension) est manquant ou vide.",
25882588
"fileNameMissing": "Le nom du fichier est vide ou manquant.",
@@ -2592,12 +2592,15 @@
25922592
"advices": {
25932593
"title": "Quelques conseils pour résoudre ce problème",
25942594
"advice1": "Vérifier que le fichier s'ouvre correctement depuis un logiciel externe.",
2595-
"advice2": "Ré-importer le fichier (vérifier les versions).",
2596-
"advice3": "Si le fichier s'ouvrait avant, essayer de restaurer une version antérieure (via l'historique du fichier)"
2595+
"advice2": "Si le fichier s'ouvrait avant, essayer de restaurer une version antérieure (via l'historique du fichier)"
25972596
},
25982597
"actions": {
25992598
"backToFiles": "Retour aux fichiers",
2600-
"retry": "Recharger la page"
2599+
"retry": "Recharger la page",
2600+
"openWithDefaultApp": "Ouvrir avec l'app par défaut",
2601+
"downloadFile": "Télécharger le fichier",
2602+
"wait": "Attendre",
2603+
"close": "Fermer"
26012604
},
26022605
"saving": {
26032606
"unsaved": "Modifications non sauvegardées",

client/src/services/cryptpad.ts

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -173,44 +173,11 @@ export class Cryptpad {
173173
throw new CryptpadOpenError(CryptpadErrorCode.DocumentTypeNotEnabled, config.documentType);
174174
}
175175

176-
// Set up a loading timeout (30 seconds) to detect stuck/corrupted files
177-
const LOADING_TIMEOUT_MS = 30000;
178-
let loadingTimeoutId: any = undefined;
179-
180-
// Create a promise that will be resolved when CryptPad successfully loads
181-
let resolveLoading!: () => void;
182-
const loadingComplete = new Promise<void>((resolve) => {
183-
resolveLoading = resolve;
184-
});
185-
186-
const timeoutPromise = new Promise<void>((_, reject) => {
187-
loadingTimeoutId = window.setTimeout(() => {
188-
const message = 'CryptPad loading timeout - the file may be corrupted, too large, or the server is not responding.';
189-
window.electronAPI.log('warn', message);
190-
reject(new CryptpadOpenError(CryptpadErrorCode.LoadingTimeout, config.documentType, message));
191-
}, LOADING_TIMEOUT_MS);
192-
});
193-
// Wrap the original onReady callback to signal successful loading
194-
const originalOnReady = config.events.onReady;
195-
config.events.onReady = () => {
196-
resolveLoading();
197-
if (originalOnReady) originalOnReady();
198-
};
199-
200176
// Store config globally so CryptPad customization can access the onError callback
201177
(window as any).cryptpadConfig = config;
202178

203-
try {
204-
// Start CryptPad API (doesn't wait for loading to complete)
205-
void (window as any).CryptPadAPI(this.containerElement.id, { ...config });
206-
207-
// Race between successful loading (onReady callback) and timeout
208-
await Promise.race([loadingComplete, timeoutPromise]);
209-
} finally {
210-
window.clearTimeout(loadingTimeoutId);
211-
// Clean up global config reference
212-
delete (window as any).cryptpadConfig;
213-
}
179+
// Start CryptPad API (doesn't wait for loading to complete)
180+
void (window as any).CryptPadAPI(this.containerElement.id, { ...config });
214181
}
215182
}
216183

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<!-- Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS -->
2+
3+
<template>
4+
<ms-modal
5+
:title="modalConfig.title"
6+
:close-button="{ visible: false }"
7+
:cancel-button="modalConfig.cancelButton"
8+
:confirm-button="modalConfig.confirmButton"
9+
>
10+
<div class="editor-error">
11+
<ms-report-text
12+
v-if="modalConfig.message"
13+
:theme="modalConfig.theme"
14+
>
15+
{{ $msTranslate(modalConfig.message) }}
16+
</ms-report-text>
17+
<div
18+
v-if="status === EditorIssueStatus.LoadingTimeout"
19+
class="editor-error-advices"
20+
>
21+
<ion-text class="editor-error-advices__title title-h5">{{ $msTranslate('fileEditors.advices.title') }}</ion-text>
22+
<ion-list class="editor-error-advices-list ion-no-padding">
23+
<ion-item class="editor-error-advices-list__item ion-no-padding body">
24+
<ion-icon
25+
class="item-icon"
26+
:icon="checkmarkCircle"
27+
/>
28+
{{ $msTranslate('fileEditors.advices.advice1') }}
29+
</ion-item>
30+
<ion-item class="editor-error-advices-list__item ion-no-padding body">
31+
<ion-icon
32+
class="item-icon"
33+
:icon="checkmarkCircle"
34+
/>
35+
{{ $msTranslate('fileEditors.advices.advice2') }}
36+
</ion-item>
37+
</ion-list>
38+
</div>
39+
</div>
40+
</ms-modal>
41+
</template>
42+
43+
<script setup lang="ts">
44+
import { isWeb } from '@/parsec';
45+
import { EditorButtonAction, EditorErrorMessage, EditorErrorTitle, EditorIssueStatus } from '@/views/files/handler/editor/types';
46+
import { IonIcon, IonItem, IonList, IonText, modalController } from '@ionic/vue';
47+
import { checkmarkCircle } from 'ionicons/icons';
48+
import { MsModal, MsModalResult, MsReportText, MsReportTheme, Translatable } from 'megashark-lib';
49+
import { computed, Ref } from 'vue';
50+
51+
const props = defineProps<{
52+
status: EditorIssueStatus;
53+
fileLoaded?: Ref<boolean>;
54+
}>();
55+
56+
interface ModalConfig {
57+
title: Translatable;
58+
message?: Translatable;
59+
theme: MsReportTheme;
60+
confirmButton: {
61+
label: Translatable;
62+
disabled: boolean;
63+
onClick: () => Promise<boolean>;
64+
};
65+
cancelButton?: {
66+
label: Translatable;
67+
disabled: boolean;
68+
};
69+
}
70+
71+
const modalConfig = computed<ModalConfig>(() => {
72+
let title: Translatable;
73+
let message: Translatable | undefined;
74+
let theme: MsReportTheme;
75+
let confirmButtonLabel: Translatable;
76+
let cancelButton: { label: Translatable; disabled: boolean } | undefined;
77+
78+
switch (props.status) {
79+
case EditorIssueStatus.EditionNotAvailable:
80+
title = EditorErrorTitle.EditionNotAvailable;
81+
message = EditorErrorMessage.EditableOnlyOnSystem;
82+
theme = MsReportTheme.Info;
83+
confirmButtonLabel = EditorButtonAction.BackToFiles;
84+
break;
85+
case EditorIssueStatus.LoadingTimeout:
86+
title = EditorErrorTitle.TooLongToOpen;
87+
message = isWeb() ? EditorErrorMessage.TooLongToOpenOnWeb : EditorErrorMessage.TooLongToOpenOnDesktop;
88+
theme = MsReportTheme.Warning;
89+
confirmButtonLabel = props.fileLoaded?.value ? EditorButtonAction.Close : EditorButtonAction.Wait;
90+
if (!props.fileLoaded?.value) {
91+
cancelButton = {
92+
label: EditorButtonAction.BackToFiles,
93+
disabled: false,
94+
};
95+
}
96+
break;
97+
case EditorIssueStatus.NetworkOffline:
98+
title = EditorErrorTitle.NetworkOffline;
99+
message = EditorErrorMessage.NetworkOffline;
100+
theme = MsReportTheme.Warning;
101+
confirmButtonLabel = EditorButtonAction.Close;
102+
break;
103+
case EditorIssueStatus.UnsupportedFileType:
104+
title = EditorErrorTitle.UnsupportedFileType;
105+
message = EditorErrorMessage.EditableOnlyOnSystem;
106+
theme = MsReportTheme.Info;
107+
confirmButtonLabel = EditorButtonAction.BackToFiles;
108+
break;
109+
default:
110+
title = EditorErrorTitle.GenericError;
111+
theme = MsReportTheme.Warning;
112+
confirmButtonLabel = EditorButtonAction.BackToFiles;
113+
break;
114+
}
115+
116+
return {
117+
title,
118+
message,
119+
theme,
120+
confirmButton: {
121+
label: confirmButtonLabel,
122+
disabled: false,
123+
onClick: async (): Promise<boolean> => {
124+
return modalController.dismiss(true, MsModalResult.Confirm);
125+
},
126+
},
127+
cancelButton,
128+
};
129+
});
130+
</script>
131+
132+
<style scoped lang="scss">
133+
.editor-error {
134+
margin-top: 0.5rem;
135+
display: flex;
136+
flex-direction: column;
137+
gap: 1.5rem;
138+
139+
&-advices {
140+
border-top: 1px solid var(--parsec-color-light-secondary-disabled);
141+
padding-top: 1.5rem;
142+
display: flex;
143+
flex-direction: column;
144+
gap: 1rem;
145+
146+
&__title {
147+
color: var(--parsec-color-light-secondary-text);
148+
}
149+
150+
&-list {
151+
padding-left: 0.5rem;
152+
list-style-type: circle;
153+
background: none;
154+
155+
&__item {
156+
margin-bottom: 0.5rem;
157+
display: flex;
158+
align-items: center;
159+
gap: 0.5rem;
160+
color: var(--parsec-color-light-secondary-soft-text);
161+
--background: none;
162+
font-size: 0.9375rem;
163+
164+
.item-icon {
165+
color: var(--parsec-color-light-secondary-grey);
166+
flex-shrink: 0;
167+
margin-right: 0.5rem;
168+
font-size: 1rem;
169+
}
170+
}
171+
}
172+
}
173+
}
174+
</style>

0 commit comments

Comments
 (0)