diff --git a/packages/app-desktop/gui/StatusScreen/StatusScreen.tsx b/packages/app-desktop/gui/StatusScreen/StatusScreen.tsx index dade2eb435c..a7eb5cf25b5 100644 --- a/packages/app-desktop/gui/StatusScreen/StatusScreen.tsx +++ b/packages/app-desktop/gui/StatusScreen/StatusScreen.tsx @@ -87,12 +87,15 @@ function StatusScreen(props: Props) { itemsHtml.push(renderSectionTitleHtml(section.title, section.title)); + let currentListKey = ''; + let listItems: any[] = []; for (const n in section.body) { if (!section.body.hasOwnProperty(n)) continue; const item = section.body[n]; let text = ''; let retryLink = null; + let itemType = null; if (typeof item === 'object') { if (item.canRetry) { const onClick = async () => { @@ -107,18 +110,40 @@ function StatusScreen(props: Props) { ); } text = item.text; + itemType = item.type; } else { text = item; } + if (itemType === 'openList') { + currentListKey = item.key; + continue; + } + + if (itemType === 'closeList') { + itemsHtml.push(); + currentListKey = ''; + listItems = []; + continue; + } + if (!text) text = '\xa0'; - itemsHtml.push( -
- {text} - {retryLink} -
- ); + if (currentListKey) { + listItems.push( +
  • + {text} + {retryLink} +
  • + ); + } else { + itemsHtml.push( +
    + {text} + {retryLink} +
    + ); + } } if (section.canRetryAll) { diff --git a/packages/lib/Synchronizer.ts b/packages/lib/Synchronizer.ts index 10ee357b05d..0f631863ffe 100644 --- a/packages/lib/Synchronizer.ts +++ b/packages/lib/Synchronizer.ts @@ -28,6 +28,22 @@ interface RemoteItem { type_?: number; } +function isCannotSyncError(error: any): boolean { + if (!error) return false; + if (['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) return true; + + // If the request times out we give up too because sometimes it's due to the + // file being large or some other connection issues, and we don't want that + // file to block the sync process. The user can choose to retry later on. + // + // message: "network timeout at: ..... + // name: "FetchError" + // type: "request-timeout" + if (error.type === 'request-timeout' || error.message.includes('network timeout')) return true; + + return false; +} + export default class Synchronizer { private db_: any; @@ -514,7 +530,7 @@ export default class Synchronizer { await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file', shareId: local.share_id }); } catch (error) { - if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) { + if (isCannotSyncError(error)) { await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message); action = null; } else { diff --git a/packages/lib/locale.ts b/packages/lib/locale.ts index d8901340e92..4b2da4b084b 100644 --- a/packages/lib/locale.ts +++ b/packages/lib/locale.ts @@ -578,7 +578,7 @@ function localesFromLanguageCode(languageCode: string, locales: string[]): strin }); } -function _(s: string, ...args: any[]) { +function _(s: string, ...args: any[]): string { const strings = localeStrings(currentLocale_); let result = strings[s]; if (result === '' || result === undefined) result = s; diff --git a/packages/lib/models/BaseItem.ts b/packages/lib/models/BaseItem.ts index ef9d7c42e91..e82a7e3bd23 100644 --- a/packages/lib/models/BaseItem.ts +++ b/packages/lib/models/BaseItem.ts @@ -756,6 +756,10 @@ export default class BaseItem extends BaseModel { return this.db().transactionExecBatch(queries); } + public static async saveSyncEnabled(itemType: ModelType, itemId: string) { + await this.db().exec('DELETE FROM sync_items WHERE item_type = ? AND item_id = ?', [itemType, itemId]); + } + // When an item is deleted, its associated sync_items data is not immediately deleted for // performance reason. So this function is used to look for these remaining sync_items and // delete them. diff --git a/packages/lib/services/ReportService.ts b/packages/lib/services/ReportService.ts index e1efe073234..adec57ec1ec 100644 --- a/packages/lib/services/ReportService.ts +++ b/packages/lib/services/ReportService.ts @@ -10,6 +10,36 @@ import Resource from '../models/Resource'; import { _ } from '../locale'; const { toTitleCase } = require('../string-utils.js'); +enum CanRetryType { + E2EE = 'e2ee', + ResourceDownload = 'resourceDownload', + ItemSync = 'itemSync', +} + +enum ReportItemType { + OpenList = 'openList', + CloseList = 'closeList', +} + +type RerportItemOrString = ReportItem | string; + +interface ReportSection { + title: string; + body: RerportItemOrString[]; + name?: string; + canRetryAll?: boolean; + retryAllHandler?: ()=> void; +} + +interface ReportItem { + type?: ReportItemType; + key?: string; + text?: string; + canRetry?: boolean; + canRetryType?: CanRetryType; + retryHandler?: ()=> void; +} + export default class ReportService { csvEscapeCell(cell: string) { cell = this.csvValueToString(cell); @@ -110,10 +140,10 @@ export default class ReportService { return output; } - async status(syncTarget: number) { + async status(syncTarget: number): Promise { const r = await this.syncStatus(syncTarget); - const sections = []; - let section: any = null; + const sections: ReportSection[] = []; + let section: ReportSection = null; const disabledItems = await BaseItem.syncDisabledItems(syncTarget); @@ -122,17 +152,29 @@ export default class ReportService { section.body.push(_('These items will remain on the device but will not be uploaded to the sync target. In order to find these items, either search for the title or the ID (which is displayed in brackets above).')); - section.body.push(''); + section.body.push({ type: ReportItemType.OpenList, key: 'disabledSyncItems' }); for (let i = 0; i < disabledItems.length; i++) { const row = disabledItems[i]; + let msg: string = ''; if (row.location === BaseItem.SYNC_ITEM_LOCATION_LOCAL) { - section.body.push(_('%s (%s) could not be uploaded: %s', row.item.title, row.item.id, row.syncInfo.sync_disabled_reason)); + msg = _('%s (%s) could not be uploaded: %s', row.item.title, row.item.id, row.syncInfo.sync_disabled_reason); } else { - section.body.push(_('Item "%s" could not be downloaded: %s', row.syncInfo.item_id, row.syncInfo.sync_disabled_reason)); + msg = _('Item "%s" could not be downloaded: %s', row.syncInfo.item_id, row.syncInfo.sync_disabled_reason); } + + section.body.push({ + text: msg, + canRetry: true, + canRetryType: CanRetryType.ItemSync, + retryHandler: async () => { + await BaseItem.saveSyncEnabled(row.item.type_, row.item.id); + }, + }); } + section.body.push({ type: ReportItemType.CloseList }); + sections.push(section); } @@ -150,7 +192,7 @@ export default class ReportService { section.body.push({ text: _('%s: %s', toTitleCase(BaseModel.modelTypeToName(row.type_)), row.id), canRetry: true, - canRetryType: 'e2ee', + canRetryType: CanRetryType.E2EE, retryHandler: async () => { await DecryptionWorker.instance().clearDisabledItem(row.type_, row.id); void DecryptionWorker.instance().scheduleStart(); @@ -158,11 +200,12 @@ export default class ReportService { }); } - const retryHandlers: any[] = []; + const retryHandlers: Function[] = []; for (let i = 0; i < section.body.length; i++) { - if (section.body[i].canRetry) { - retryHandlers.push(section.body[i].retryHandler); + const item: RerportItemOrString = section.body[i]; + if (typeof item !== 'string' && item.canRetry) { + retryHandlers.push(item.retryHandler); } } @@ -210,7 +253,7 @@ export default class ReportService { section.body.push({ text: _('%s (%s): %s', row.resource_title, row.resource_id, row.fetch_error), canRetry: true, - canRetryType: 'resourceDownload', + canRetryType: CanRetryType.ResourceDownload, retryHandler: async () => { await Resource.resetErrorStatus(row.resource_id); void ResourceFetcher.instance().autoAddResources();