From 71d789da89f85c8fe65e56e52f2cff0778e36fa5 Mon Sep 17 00:00:00 2001 From: Lucian Cerbu Date: Tue, 21 Apr 2026 15:06:38 +0300 Subject: [PATCH 1/3] Changed the thumbnails to 256 size; the new thumbnail size was added from the backend. --- .../Base/SwiftUIViews/SectionHeaderView.swift | 2 +- Permanent/Common/Models/Data/ArchiveVO.swift | 10 +- .../Common/Models/Data/Folder/FolderVO.swift | 9 +- .../Models/Data/Folder/MinFolderVO.swift | 9 +- .../Common/Models/Data/FolderV2Models.swift | 5 +- Permanent/Common/Models/Data/ItemVO.swift | 9 +- .../Common/Models/Data/ParentFolderVO.swift | 9 +- .../Common/Models/Data/RecordV2Models.swift | 23 ++ Permanent/Common/Models/Data/RecordVO.swift | 9 +- .../Scenes/Share Preview/Models/FileVM.swift | 2 +- .../Archives/Cells/ArchiveTableViewCell.swift | 2 +- .../ArchivesViewController.swift | 2 +- .../MetadataEditFileNamesViewModel.swift | 2 +- .../Views/MetadataEditFileNamesView.swift | 2 +- .../EditMetadata/Views/MetadataEditView.swift | 2 +- .../FileDetailsTopCollectionViewCell.swift | 2 +- .../FilePreviewViewController.swift | 6 +- .../OnboardingContainerViewModel.swift | 3 +- .../OnboardingWhatsImportantViewModel.swift | 2 +- .../PublicArchiveViewController.swift | 4 +- .../PublicProfilePageViewController.swift | 1 + .../Shares/SwiftUIViews/ShareItemView.swift | 2 +- .../ViewModels/ShareItemViewModel.swift | 4 +- .../SharePreviewSwiftUIViewModel.swift | 7 +- .../SideMenuViewController.swift | 2 +- .../ViewModels/ViewModel/FileModel.swift | 52 ++-- .../ViewModel/FilePreviewViewModel.swift | 2 +- PermanentTests/SessionTests.swift | 2 +- PermanentTests/ShareExtensionTests.swift | 4 +- PermanentTests/ShareItemViewModelTests.swift | 229 +++++++++++++++++- .../SharePreviewSwiftUIViewModelTests.swift | 19 +- .../SharePreviewViewModelTests.swift | 2 + 32 files changed, 375 insertions(+), 65 deletions(-) diff --git a/Permanent/Common/Base/SwiftUIViews/SectionHeaderView.swift b/Permanent/Common/Base/SwiftUIViews/SectionHeaderView.swift index e2d4df44..a2f4c3d9 100644 --- a/Permanent/Common/Base/SwiftUIViews/SectionHeaderView.swift +++ b/Permanent/Common/Base/SwiftUIViews/SectionHeaderView.swift @@ -14,7 +14,7 @@ struct SectionHeaderView: View { VStack { HStack { let files = selectedFiles - if !files.isEmpty, let url = URL(string: files.first?.thumbnailURL500) { + if !files.isEmpty, let url = URL(string: files.first?.preferredThumbnailURL) { WebImage(url: url) .resizable() .placeholder(content: { diff --git a/Permanent/Common/Models/Data/ArchiveVO.swift b/Permanent/Common/Models/Data/ArchiveVO.swift index 47bf47c6..287cc3cf 100644 --- a/Permanent/Common/Models/Data/ArchiveVO.swift +++ b/Permanent/Common/Models/Data/ArchiveVO.swift @@ -46,6 +46,7 @@ struct ArchiveVOData: Model { let type: String? let thumbStatus: Status? let imageRatio: JSONAny? + let thumbnail256: String? let thumbURL200: String? let thumbURL500: String? let thumbURL1000: String? @@ -64,7 +65,13 @@ struct ArchiveVOData: Model { case archiveID = "archiveId" case publicDT, archiveNbr case archiveVOPublic = "public" - case view, viewProperty, vaultKey, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, createdDT, updatedDT + case view, viewProperty, vaultKey, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbnail256, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, createdDT, updatedDT + } +} + +extension ArchiveVOData { + var preferredThumbnailURL: String? { + thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 } } @@ -99,6 +106,7 @@ extension ArchiveVOData { type: "archive", thumbStatus: .ok, imageRatio: nil, + thumbnail256: nil, thumbURL200: "https://example.com/thumb200.jpg", thumbURL500: "https://example.com/thumb500.jpg", thumbURL1000: "https://example.com/thumb1000.jpg", diff --git a/Permanent/Common/Models/Data/Folder/FolderVO.swift b/Permanent/Common/Models/Data/Folder/FolderVO.swift index 7666bbb5..94df3b0b 100644 --- a/Permanent/Common/Models/Data/Folder/FolderVO.swift +++ b/Permanent/Common/Models/Data/Folder/FolderVO.swift @@ -31,6 +31,7 @@ struct FolderVOData: Model { let viewProperty, thumbArchiveNbr: String? let type, thumbStatus: String? let imageRatio: JSONAny? + let thumbnail256: String? let thumbURL200: String? let thumbURL500: String? let thumbURL1000: String? @@ -74,7 +75,7 @@ struct FolderVOData: Model { case special, sort case locnID = "locnId" case timeZoneID = "timeZoneId" - case view, viewProperty, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, publicDT + case view, viewProperty, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbnail256, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, publicDT case parentFolderID = "parentFolderId" case folderLinkType = "folder_linkType" case folderLinkVOS = "FolderLinkVOs" @@ -102,3 +103,9 @@ struct FolderVOData: Model { case archiveArchiveNbr, returnDataSize, posStart, posLimit, searchScore, createdDT, updatedDT } } + +extension FolderVOData { + var preferredThumbnailURL: String? { + thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + } +} diff --git a/Permanent/Common/Models/Data/Folder/MinFolderVO.swift b/Permanent/Common/Models/Data/Folder/MinFolderVO.swift index a3404ae1..f1a43ea7 100644 --- a/Permanent/Common/Models/Data/Folder/MinFolderVO.swift +++ b/Permanent/Common/Models/Data/Folder/MinFolderVO.swift @@ -23,6 +23,7 @@ struct MinFolderVO: Codable { let viewProperty, thumbArchiveNbr: JSONAny? let type, thumbStatus: String? let imageRatio: JSONAny? + let thumbnail256: String? let thumbURL200: String? let thumbURL500: String? let thumbURL1000: String? @@ -67,7 +68,7 @@ struct MinFolderVO: Codable { case special, sort case locnID = "locnId" case timeZoneID = "timeZoneId" - case view, viewProperty, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, publicDT + case view, viewProperty, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbnail256, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, publicDT case parentFolderID = "parentFolderId" case folderLinkType = "folder_linkType" case folderLinkVOS = "FolderLinkVOs" @@ -95,3 +96,9 @@ struct MinFolderVO: Codable { case archiveArchiveNbr, returnDataSize, posStart, posLimit, searchScore, createdDT, updatedDT } } + +extension MinFolderVO { + var preferredThumbnailURL: String? { + thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + } +} diff --git a/Permanent/Common/Models/Data/FolderV2Models.swift b/Permanent/Common/Models/Data/FolderV2Models.swift index 2e35674e..0bdde827 100644 --- a/Permanent/Common/Models/Data/FolderV2Models.swift +++ b/Permanent/Common/Models/Data/FolderV2Models.swift @@ -83,12 +83,14 @@ struct FolderPathsV2: Model { } struct ThumbnailUrlsV2: Model { + let url256: String? let url200: String? let url500: String? let url1000: String? let url2000: String? enum CodingKeys: String, CodingKey { + case url256 = "256" case url200 = "200" case url500 = "500" case url1000 = "1000" @@ -122,6 +124,7 @@ struct FolderChildV2Data: Model { let description: String? let downloadName: String? let uploadFileName: String? + let thumbnail256: String? let thumbUrl200: String? let thumbUrl500: String? let thumbUrl1000: String? @@ -145,7 +148,7 @@ struct FolderChildV2Data: Model { /// Returns the best available thumbnail URL var bestThumbnailURL: String? { - return thumbUrl500 ?? thumbUrl200 ?? thumbUrl1000 + thumbnail256 ?? thumbUrl500 ?? thumbUrl200 ?? thumbUrl1000 ?? thumbUrl2000 } } diff --git a/Permanent/Common/Models/Data/ItemVO.swift b/Permanent/Common/Models/Data/ItemVO.swift index 194554a0..631025cd 100644 --- a/Permanent/Common/Models/Data/ItemVO.swift +++ b/Permanent/Common/Models/Data/ItemVO.swift @@ -21,6 +21,7 @@ struct ItemVO: Model { let view: String? let viewProperty, thumbArchiveNbr: JSONAny? let imageRatio, type, thumbStatus: String? + let thumbnail256: String? let thumbURL200: String? let thumbURL500: String? let thumbURL1000: String? @@ -70,7 +71,7 @@ struct ItemVO: Model { case special, sort case locnID = "locnId" case timeZoneID = "timeZoneId" - case view, viewProperty, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, publicDT + case view, viewProperty, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbnail256, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, publicDT case parentFolderID = "parentFolderId" case folderLinkType = "folder_linkType" case folderLinkVOS = "FolderLinkVOs" @@ -110,3 +111,9 @@ struct ItemVO: Model { case createdDT, updatedDT } } + +extension ItemVO { + var preferredThumbnailURL: String? { + thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + } +} diff --git a/Permanent/Common/Models/Data/ParentFolderVO.swift b/Permanent/Common/Models/Data/ParentFolderVO.swift index da20e605..a80db7bd 100644 --- a/Permanent/Common/Models/Data/ParentFolderVO.swift +++ b/Permanent/Common/Models/Data/ParentFolderVO.swift @@ -23,6 +23,7 @@ struct ParentFolderVO: Model { let viewProperty, thumbArchiveNbr: JSONAny? let type, thumbStatus: String? let imageRatio: JSONAny? + let thumbnail256: String? let thumbURL200: String? let thumbURL500: String? let thumbURL1000: String? @@ -63,7 +64,7 @@ struct ParentFolderVO: Model { case special, sort case locnID = "locnId" case timeZoneID = "timeZoneId" - case view, viewProperty, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, publicDT + case view, viewProperty, thumbArchiveNbr, imageRatio, type, thumbStatus, thumbnail256, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, status, publicDT case parentFolderID = "parentFolderId" case folderLinkType = "folder_linkType" case folderLinkVOS = "FolderLinkVOs" @@ -91,3 +92,9 @@ struct ParentFolderVO: Model { case archiveArchiveNbr, returnDataSize, posStart, posLimit, searchScore, createdDT, updatedDT } } + +extension ParentFolderVO { + var preferredThumbnailURL: String? { + thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + } +} diff --git a/Permanent/Common/Models/Data/RecordV2Models.swift b/Permanent/Common/Models/Data/RecordV2Models.swift index e188c319..fc849621 100644 --- a/Permanent/Common/Models/Data/RecordV2Models.swift +++ b/Permanent/Common/Models/Data/RecordV2Models.swift @@ -29,6 +29,7 @@ struct RecordV2Data: Model { let displayTimeInEDTF: String? let fileCreatedAt: String? let imageRatio: Double? + let thumbnail256: String? let thumbUrl200: String? let thumbUrl500: String? let thumbUrl1000: String? @@ -52,6 +53,16 @@ struct RecordV2Data: Model { let archive: RecordArchiveV2? } +extension RecordV2Data { + var resolvedThumbnail256: String? { + thumbnail256 ?? thumbnailUrls?.url256 + } + + var preferredThumbnailURL: String? { + resolvedThumbnail256 ?? thumbUrl500 ?? thumbUrl200 ?? thumbUrl1000 ?? thumbUrl2000 + } +} + struct LocationV2: Model { let id: String? let streetNumber: String? @@ -88,6 +99,7 @@ struct RecordShareV2: Model { struct RecordShareArchiveV2: Model { let archiveId: String? + let thumbnail256: String? let thumbUrl200: String? let thumbUrl500: String? let thumbUrl1000: String? @@ -97,6 +109,7 @@ struct RecordShareArchiveV2: Model { enum CodingKeys: String, CodingKey { case archiveId = "id" // Map "id" from JSON to archiveId + case thumbnail256 case thumbUrl200 case thumbUrl500 case thumbUrl1000 @@ -106,6 +119,16 @@ struct RecordShareArchiveV2: Model { } } +extension RecordShareArchiveV2 { + var resolvedThumbnail256: String? { + thumbnail256 ?? thumbnailUrls?.url256 + } + + var preferredThumbnailURL: String? { + resolvedThumbnail256 ?? thumbUrl500 ?? thumbUrl200 ?? thumbUrl1000 ?? thumbUrl2000 + } +} + struct RecordArchiveV2: Model { let id: String? let archiveNumber: String? diff --git a/Permanent/Common/Models/Data/RecordVO.swift b/Permanent/Common/Models/Data/RecordVO.swift index 8439e9d7..e81897a9 100644 --- a/Permanent/Common/Models/Data/RecordVO.swift +++ b/Permanent/Common/Models/Data/RecordVO.swift @@ -29,6 +29,7 @@ struct RecordVOData: Model { let encryption, metaToken: String? let refArchiveNbr: JSONAny? let type, thumbStatus: String? + let thumbnail256: String? let thumbURL200, thumbURL500, thumbURL1000, thumbURL2000: String? let thumbDT, fileStatus: String? let status: String? @@ -69,7 +70,7 @@ struct RecordVOData: Model { case displayDT, displayEndDT, derivedDT, derivedEndDT, derivedCreatedDT case locnID = "locnId" case timeZoneID = "timeZoneId" - case view, viewProperty, imageRatio, encryption, metaToken, refArchiveNbr, type, thumbStatus, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, fileStatus, status, processedDT + case view, viewProperty, imageRatio, encryption, metaToken, refArchiveNbr, type, thumbStatus, thumbnail256, thumbURL200, thumbURL500, thumbURL1000, thumbURL2000, thumbDT, fileStatus, status, processedDT case folderLinkVOS = "FolderLinkVOs" case folderLinkID = "folder_linkId" case parentFolderID = "parentFolderId" @@ -99,3 +100,9 @@ struct RecordVOData: Model { case searchScore, archiveArchiveNbr, createdDT, updatedDT } } + +extension RecordVOData { + var preferredThumbnailURL: String? { + thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + } +} diff --git a/Permanent/Common/Scenes/Share Preview/Models/FileVM.swift b/Permanent/Common/Scenes/Share Preview/Models/FileVM.swift index a1adb78f..8b3af950 100644 --- a/Permanent/Common/Scenes/Share Preview/Models/FileVM.swift +++ b/Permanent/Common/Scenes/Share Preview/Models/FileVM.swift @@ -21,7 +21,7 @@ struct FileVM: File { init(record: RecordVOData) { name = record.displayName ?? "" date = record.displayDT ?? "" - thumbStringURL = record.thumbURL500 ?? "" + thumbStringURL = record.preferredThumbnailURL ?? "" } } diff --git a/Permanent/Modules/Archives/Cells/ArchiveTableViewCell.swift b/Permanent/Modules/Archives/Cells/ArchiveTableViewCell.swift index 12bba182..9f765845 100644 --- a/Permanent/Modules/Archives/Cells/ArchiveTableViewCell.swift +++ b/Permanent/Modules/Archives/Cells/ArchiveTableViewCell.swift @@ -44,7 +44,7 @@ class ArchiveTableViewCell: UITableViewCell { } func updateCell(model: ShareVOData) { - archiveImageView.load(urlString: model.archiveVO?.thumbURL200 ?? "") + archiveImageView.load(urlString: model.archiveVO?.preferredThumbnailURL ?? "") archiveNameLabel.text = .init(format: .archiveName, model.archiveVO?.fullName ?? "") if ShareStatus.status(forValue: model.status ?? "") != .pending { diff --git a/Permanent/Modules/Archives/ViewController/ArchivesViewController.swift b/Permanent/Modules/Archives/ViewController/ArchivesViewController.swift index aa0f4c40..b380cd00 100644 --- a/Permanent/Modules/Archives/ViewController/ArchivesViewController.swift +++ b/Permanent/Modules/Archives/ViewController/ArchivesViewController.swift @@ -376,7 +376,7 @@ extension ArchivesViewController: UITableViewDataSource, UITableViewDelegate { let archiveVO = tableViewData[indexPath.row] tableViewCell.updateCell(withArchiveVO: archiveVO, isDefault: archiveVO.archiveID == viewModel?.defaultArchiveId, isManaging: isManaging) - tableViewCell.rightButtonAction = rightButtonAction(archiveName: archiveVO.fullName ?? "", archiveThumbnail: archiveVO.thumbURL200 ?? "", archive: archiveVO) + tableViewCell.rightButtonAction = rightButtonAction(archiveName: archiveVO.fullName ?? "", archiveThumbnail: archiveVO.preferredThumbnailURL ?? "", archive: archiveVO) cell = tableViewCell } diff --git a/Permanent/Modules/EditMetadata/ViewModels/MetadataEditFileNamesViewModel.swift b/Permanent/Modules/EditMetadata/ViewModels/MetadataEditFileNamesViewModel.swift index 0c8c753d..84467dd1 100644 --- a/Permanent/Modules/EditMetadata/ViewModels/MetadataEditFileNamesViewModel.swift +++ b/Permanent/Modules/EditMetadata/ViewModels/MetadataEditFileNamesViewModel.swift @@ -33,7 +33,7 @@ class MetadataEditFileNamesViewModel: ObservableObject { self.selectedFiles = selectedFiles self.hasUpdates = hasUpdates - imagePreviewURL = selectedFiles.first?.thumbnailURL500 + imagePreviewURL = selectedFiles.first?.preferredThumbnailURL fileNamePreview = selectedFiles.first?.name if let size = selectedFiles.first?.size { fileSizePreview = size.bytesToReadableForm(useDecimal: true) diff --git a/Permanent/Modules/EditMetadata/Views/MetadataEditFileNamesView.swift b/Permanent/Modules/EditMetadata/Views/MetadataEditFileNamesView.swift index 116deab4..9d63d8da 100644 --- a/Permanent/Modules/EditMetadata/Views/MetadataEditFileNamesView.swift +++ b/Permanent/Modules/EditMetadata/Views/MetadataEditFileNamesView.swift @@ -184,7 +184,7 @@ struct MetadataEditFileNames_Previews: PreviewProvider { @State static var hasUpdates: Bool = true static var previews: some View { - let file = FileModel(model: FolderVOData(folderID: 22, archiveNbr: nil, archiveID: 22, displayName: "TestFile", displayDT: nil, displayEndDT: nil, derivedDT: nil, derivedEndDT: nil, note: nil, voDescription: nil, special: nil, sort: nil, locnID: nil, timeZoneID: nil, view: nil, viewProperty: nil, thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: "https://img.freepik.com/free-photo/bright-yellow-fire-blazing-against-night-sky-generated-by-ai_188544-11620.jpg?t=st=1690878101~exp=1690881701~hmac=103cd63a2a40c4feeda570cad19c0c3cc8de275d6d6c2731ee33c3310669f67c&w=2000", thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, status: nil, publicDT: nil, parentFolderID: nil, folderLinkType: nil, folderLinkVOS: nil, accessRole: nil, position: nil, pathAsFolderLinkID: nil, shareDT: nil, pathAsText: nil, folderLinkID: nil, parentFolderLinkID: nil, parentFolderVOS: nil, parentArchiveNbr: nil, parentDisplayName: nil, pathAsArchiveNbr: nil, childFolderVOS: nil, recordVOS: nil, locnVO: nil, timezoneVO: nil, directiveVOS: nil, tagVOS: nil, sharedArchiveVOS: nil, folderSizeVO: nil, attachmentRecordVOS: nil, hasAttachments: nil, childItemVOS: nil, shareVOS: nil, accessVO: nil, returnDataSize: nil, archiveArchiveNbr: nil, accessVOS: nil, posStart: nil, posLimit: nil, searchScore: nil, createdDT: nil, updatedDT: nil)) + let file = FileModel(model: FolderVOData(folderID: 22, archiveNbr: nil, archiveID: 22, displayName: "TestFile", displayDT: nil, displayEndDT: nil, derivedDT: nil, derivedEndDT: nil, note: nil, voDescription: nil, special: nil, sort: nil, locnID: nil, timeZoneID: nil, view: nil, viewProperty: nil, thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: "https://img.freepik.com/free-photo/bright-yellow-fire-blazing-against-night-sky-generated-by-ai_188544-11620.jpg?t=st=1690878101~exp=1690881701~hmac=103cd63a2a40c4feeda570cad19c0c3cc8de275d6d6c2731ee33c3310669f67c&w=2000", thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, status: nil, publicDT: nil, parentFolderID: nil, folderLinkType: nil, folderLinkVOS: nil, accessRole: nil, position: nil, pathAsFolderLinkID: nil, shareDT: nil, pathAsText: nil, folderLinkID: nil, parentFolderLinkID: nil, parentFolderVOS: nil, parentArchiveNbr: nil, parentDisplayName: nil, pathAsArchiveNbr: nil, childFolderVOS: nil, recordVOS: nil, locnVO: nil, timezoneVO: nil, directiveVOS: nil, tagVOS: nil, sharedArchiveVOS: nil, folderSizeVO: nil, attachmentRecordVOS: nil, hasAttachments: nil, childItemVOS: nil, shareVOS: nil, accessVO: nil, returnDataSize: nil, archiveArchiveNbr: nil, accessVOS: nil, posStart: nil, posLimit: nil, searchScore: nil, createdDT: nil, updatedDT: nil)) MetadataEditFileNamesView(viewModel: MetadataEditFileNamesViewModel(selectedFiles: [file], hasUpdates: $hasUpdates)) } } diff --git a/Permanent/Modules/EditMetadata/Views/MetadataEditView.swift b/Permanent/Modules/EditMetadata/Views/MetadataEditView.swift index 4b6236c3..f62db7f2 100644 --- a/Permanent/Modules/EditMetadata/Views/MetadataEditView.swift +++ b/Permanent/Modules/EditMetadata/Views/MetadataEditView.swift @@ -211,7 +211,7 @@ struct MetadataEditView: View { struct MetadataEditView_Previews: PreviewProvider { static var previews: some View { - let file = FileModel(model: FolderVOData(folderID: 22, archiveNbr: nil, archiveID: 22, displayName: "TestFile", displayDT: nil, displayEndDT: nil, derivedDT: nil, derivedEndDT: nil, note: nil, voDescription: nil, special: nil, sort: nil, locnID: nil, timeZoneID: nil, view: nil, viewProperty: nil, thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: "https://img.freepik.com/free-photo/bright-yellow-fire-blazing-against-night-sky-generated-by-ai_188544-11620.jpg?t=st=1690878101~exp=1690881701~hmac=103cd63a2a40c4feeda570cad19c0c3cc8de275d6d6c2731ee33c3310669f67c&w=2000", thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, status: nil, publicDT: nil, parentFolderID: nil, folderLinkType: nil, folderLinkVOS: nil, accessRole: nil, position: nil, pathAsFolderLinkID: nil, shareDT: nil, pathAsText: nil, folderLinkID: nil, parentFolderLinkID: nil, parentFolderVOS: nil, parentArchiveNbr: nil, parentDisplayName: nil, pathAsArchiveNbr: nil, childFolderVOS: nil, recordVOS: nil, locnVO: nil, timezoneVO: nil, directiveVOS: nil, tagVOS: nil, sharedArchiveVOS: nil, folderSizeVO: nil, attachmentRecordVOS: nil, hasAttachments: nil, childItemVOS: nil, shareVOS: nil, accessVO: nil, returnDataSize: nil, archiveArchiveNbr: nil, accessVOS: nil, posStart: nil, posLimit: nil, searchScore: nil, createdDT: nil, updatedDT: nil)) + let file = FileModel(model: FolderVOData(folderID: 22, archiveNbr: nil, archiveID: 22, displayName: "TestFile", displayDT: nil, displayEndDT: nil, derivedDT: nil, derivedEndDT: nil, note: nil, voDescription: nil, special: nil, sort: nil, locnID: nil, timeZoneID: nil, view: nil, viewProperty: nil, thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: "https://img.freepik.com/free-photo/bright-yellow-fire-blazing-against-night-sky-generated-by-ai_188544-11620.jpg?t=st=1690878101~exp=1690881701~hmac=103cd63a2a40c4feeda570cad19c0c3cc8de275d6d6c2731ee33c3310669f67c&w=2000", thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, status: nil, publicDT: nil, parentFolderID: nil, folderLinkType: nil, folderLinkVOS: nil, accessRole: nil, position: nil, pathAsFolderLinkID: nil, shareDT: nil, pathAsText: nil, folderLinkID: nil, parentFolderLinkID: nil, parentFolderVOS: nil, parentArchiveNbr: nil, parentDisplayName: nil, pathAsArchiveNbr: nil, childFolderVOS: nil, recordVOS: nil, locnVO: nil, timezoneVO: nil, directiveVOS: nil, tagVOS: nil, sharedArchiveVOS: nil, folderSizeVO: nil, attachmentRecordVOS: nil, hasAttachments: nil, childItemVOS: nil, shareVOS: nil, accessVO: nil, returnDataSize: nil, archiveArchiveNbr: nil, accessVOS: nil, posStart: nil, posLimit: nil, searchScore: nil, createdDT: nil, updatedDT: nil)) MetadataEditView(viewModel: FilesMetadataViewModel(files: [file])) } } diff --git a/Permanent/Modules/FileOperations/Cells/FileDetailsTopCollectionViewCell.swift b/Permanent/Modules/FileOperations/Cells/FileDetailsTopCollectionViewCell.swift index 51a4067e..e9fc30b5 100644 --- a/Permanent/Modules/FileOperations/Cells/FileDetailsTopCollectionViewCell.swift +++ b/Permanent/Modules/FileOperations/Cells/FileDetailsTopCollectionViewCell.swift @@ -24,7 +24,7 @@ class FileDetailsTopCollectionViewCell: FileDetailsBaseCollectionViewCell { activityIndicator.startAnimating() imageView.image = nil - let urlString = viewModel.file.thumbnailURL2000 ?? viewModel.fileThumbnailURL() ?? "" + let urlString = viewModel.file.preferredThumbnailURL ?? viewModel.fileThumbnailURL() ?? "" guard let url = URL(string: urlString) else { return } imageView.sd_setImage(with: url) { image, error, cacheType, url in diff --git a/Permanent/Modules/FileOperations/ViewController/FilePreviewViewController.swift b/Permanent/Modules/FileOperations/ViewController/FilePreviewViewController.swift index f75c022d..d6a8cab0 100644 --- a/Permanent/Modules/FileOperations/ViewController/FilePreviewViewController.swift +++ b/Permanent/Modules/FileOperations/ViewController/FilePreviewViewController.swift @@ -75,7 +75,7 @@ class FilePreviewViewController: BaseViewController { if isViewLoaded { activityIndicator.startAnimating() - if let url = URL(string: file.thumbnailURL) { + if let url = URL(string: file.preferredThumbnailURL) { thumbnailImageView.sd_setImage(with: url) } } @@ -83,7 +83,7 @@ class FilePreviewViewController: BaseViewController { if viewModel == nil || viewModel?.recordVO == nil { viewModel = FilePreviewViewModel(file: file) - if file.type == .image, let url = URL(string: file.thumbnailURL2000) { + if file.type == .image, let url = URL(string: file.preferredThumbnailURL) { loadImage(withURL: url) } @@ -98,7 +98,7 @@ class FilePreviewViewController: BaseViewController { self?.retryButton.isHidden = false } }) - } else if file.type == .image, let url = URL(string: file.thumbnailURL2000) { + } else if file.type == .image, let url = URL(string: file.preferredThumbnailURL) { loadImage(withURL: url) } else { loadRecord() diff --git a/Permanent/Modules/Onboarding/Screens/Container/OnboardingContainerViewModel.swift b/Permanent/Modules/Onboarding/Screens/Container/OnboardingContainerViewModel.swift index 54892ef7..8f79954d 100644 --- a/Permanent/Modules/Onboarding/Screens/Container/OnboardingContainerViewModel.swift +++ b/Permanent/Modules/Onboarding/Screens/Container/OnboardingContainerViewModel.swift @@ -71,7 +71,7 @@ class OnboardingContainerViewModel: ObservableObject { let status = archive.archiveVO?.status, let archiveID = archive.archiveVO?.archiveID, status == ArchiveVOData.Status.pending || status == ArchiveVOData.Status.ok { - allArchives.append(OnboardingArchive(fullname: fullName, accessType: AccessRole.roleForValue(archive.archiveVO?.accessRole).groupName, status: status, archiveID: archiveID, thumbnailURL: archive.archiveVO?.thumbURL200 ?? "", isThumbnailGenerated: archive.archiveVO?.thumbStatus != .genAvatar ? true : false)) + allArchives.append(OnboardingArchive(fullname: fullName, accessType: AccessRole.roleForValue(archive.archiveVO?.accessRole).groupName, status: status, archiveID: archiveID, thumbnailURL: archive.archiveVO?.preferredThumbnailURL ?? "", isThumbnailGenerated: archive.archiveVO?.thumbStatus != .genAvatar ? true : false)) } } } else { @@ -98,4 +98,3 @@ class OnboardingContainerViewModel: ObservableObject { updateAccountOperation.execute(in: APIRequestDispatcher()) {_ in} } } - diff --git a/Permanent/Modules/Onboarding/Screens/WhatsImportant/OnboardingWhatsImportantViewModel.swift b/Permanent/Modules/Onboarding/Screens/WhatsImportant/OnboardingWhatsImportantViewModel.swift index 6c280a76..61016016 100644 --- a/Permanent/Modules/Onboarding/Screens/WhatsImportant/OnboardingWhatsImportantViewModel.swift +++ b/Permanent/Modules/Onboarding/Screens/WhatsImportant/OnboardingWhatsImportantViewModel.swift @@ -221,7 +221,7 @@ class OnboardingWhatsImportantViewModel: ObservableObject { let status = archive.archiveVO?.status, let archiveID = archive.archiveVO?.archiveID, status == ArchiveVOData.Status.ok { - containerViewModel.allArchives.append(OnboardingArchive(fullname: fullName, accessType: AccessRole.roleForValue(archive.archiveVO?.accessRole).groupName, status: status, archiveID: archiveID, thumbnailURL: archive.archiveVO?.thumbURL200 ?? "", isThumbnailGenerated: archive.archiveVO?.thumbStatus != .genAvatar ? true : false)) + containerViewModel.allArchives.append(OnboardingArchive(fullname: fullName, accessType: AccessRole.roleForValue(archive.archiveVO?.accessRole).groupName, status: status, archiveID: archiveID, thumbnailURL: archive.archiveVO?.preferredThumbnailURL ?? "", isThumbnailGenerated: archive.archiveVO?.thumbStatus != .genAvatar ? true : false)) } } } else { diff --git a/Permanent/Modules/PublicProfile/ViewController/PublicArchiveViewController.swift b/Permanent/Modules/PublicProfile/ViewController/PublicArchiveViewController.swift index 6d6d537c..254d93dc 100644 --- a/Permanent/Modules/PublicProfile/ViewController/PublicArchiveViewController.swift +++ b/Permanent/Modules/PublicProfile/ViewController/PublicArchiveViewController.swift @@ -421,7 +421,7 @@ extension PublicArchiveViewController: MyFilesViewModelPickerDelegate { if self.isPickingProfilePicture { self.viewModel?.updateProfilePicture(file: file, then: { status in self.dismiss(animated: true, completion: { - if status == .success, let thumbURL = URL(string: file.thumbnailURL2000) { + if status == .success, let thumbURL = URL(string: file.preferredThumbnailURL) { self.profilePhotoImageView.sd_setImage(with: thumbURL) AuthenticationManager.shared.syncSession { [weak self] status in switch status { @@ -439,7 +439,7 @@ extension PublicArchiveViewController: MyFilesViewModelPickerDelegate { } else { self.viewModel?.updateBanner(thumbArchiveNbr: file.archiveNo, then: { status in self.dismiss(animated: true, completion: { - if status == .success, let thumbURL = URL(string: file.thumbnailURL2000) { + if status == .success, let thumbURL = URL(string: file.preferredThumbnailURL) { self.profileBannerImageView.sd_setImage(with: thumbURL) } else { self.showErrorAlert(message: .errorMessage) diff --git a/Permanent/Modules/PublicProfile/ViewController/PublicProfilePageViewController.swift b/Permanent/Modules/PublicProfile/ViewController/PublicProfilePageViewController.swift index 49c400f4..2fd969b1 100644 --- a/Permanent/Modules/PublicProfile/ViewController/PublicProfilePageViewController.swift +++ b/Permanent/Modules/PublicProfile/ViewController/PublicProfilePageViewController.swift @@ -310,6 +310,7 @@ class PublicProfilePageViewController: BaseViewController String? { - let stringURL: String? = recordVO?.recordVO?.thumbURL2000 + let stringURL: String? = recordVO?.recordVO?.preferredThumbnailURL return stringURL } diff --git a/PermanentTests/SessionTests.swift b/PermanentTests/SessionTests.swift index c2dd4afc..d4f97362 100644 --- a/PermanentTests/SessionTests.swift +++ b/PermanentTests/SessionTests.swift @@ -15,7 +15,7 @@ class SessionTests: XCTestCase { let accountVO = AccountVOData(accountID: 1000, primaryEmail: "email@email.com", fullName: "Test Account", address: "Street", address2: nil, country: nil, city: nil, state: nil, zip: nil, primaryPhone: nil, level: nil, apiToken: nil, betaParticipant: nil, facebookAccountID: nil, googleAccountID: nil, status: nil, type: nil, emailStatus: nil, phoneStatus: nil, notificationPreferences: nil, agreed: nil, optIn: nil, emailArray: nil, inviteCode: nil, rememberMe: nil, keepLoggedIn: nil, accessRole: nil, spaceTotal: nil, spaceLeft: nil, fileTotal: nil, fileLeft: nil, changePrimaryEmail: nil, changePrimaryPhone: nil, createdDT: "2021-01-27T19:48:08", updatedDT: nil, hideChecklist: false) - let archiveVO = ArchiveVOData(childFolderVOS: nil, folderSizeVOS: nil, recordVOS: nil, accessRole: "access.role.owner", fullName: "test name", spaceTotal: nil, spaceLeft: nil, fileTotal: nil, fileLeft: nil, relationType: nil, homeCity: nil, homeState: nil, homeCountry: nil, itemVOS: nil, birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 1653, publicDT: "2021-01-27T19:48:08", archiveNbr: "00in-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: "00in-000v", type: "type.archive.person", thumbStatus: ArchiveVOData.Status.ok, imageRatio: nil, thumbURL200: "https://stagingcdn.permanent.org/00in-0000.thumb.w200?t=1686306811&Expires=1686306811&Signature=ZXjq08upvH73kyiLZJb-IYlSO4Jz6SaICjGBwHlL-UaQqpd1VAK6KWKnkIQtrLgsfhdkUhwa-TZT5tBSOhFDoeXfxOSLEEF19ml0W~rhyWjXpxhbMhqCL3s43lQ4p0uTeo4KuGXxx6-egFIZK2Z-5hzL52e5tpUJ6r5gENiiSL5r02ZapGmnKDN-UmF6vxaGLYrIAFZ5CpIuV6zCLjaKjLl-P2Ehp~LVTogJF9Tq7vibVTwYcagN4dnO9iDRp4u2alv0SceW2n8NavGaby1tnvQekVnqajbL0Utl1s3kUxiZ2V9VQmGrNRrZ4RRC8lB1xG8hDlb4GpUaQ0H86xiESg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL500: "https://stagingcdn.permanent.org/00in-0000.thumb.w500?t=1686306811&Expires=1686306811&Signature=e5W18KBakMSwIr5EHohYgHf3Nz23VA7eb6-x-3D1NH6Lsq-hxJyIHf9CTCmcksK-HR-CJKyrnIDjA2frUwpT274sPQ4fqVbXh~Od5i7Nh2gv9Gogs73z2QZ5a4Vrnuxhl6ncxWlVixs8AiUu~3ZgjfGQjmk2YODr8aIfp2siAg3SoN0NG1tHQ5AY7QfzrnXRCx55-~g3DaPcU8nuOfhwHAmlQzzvkortQIT2v2OP81~Kh8FW64go7Da~L~4BLPclq2xBcgl9Nk3NKMDIsCPVcVOOOTODz5yvwUbrhxZ7MmPHSxaGxLe8hvJqbrWP~hWgcouM-BLMvFupsRe1thYQhA__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL1000: "https://stagingcdn.permanent.org/00in-0000.thumb.w1000?t=1686306811&Expires=1686306811&Signature=Y72DJ9LSrpWQIy2UkUWGil6ZkKaOl2CDJMD9mhxKjl7uPgDpcb3h8KKnjI34~1GyRgsDbB~rE2-TjhO2y7zm2YMfhHEu7yggh8F8wpVBJnETi-O4R0sceWu3pgeSXkyQ5rW~I3mkGPRV8IyoC2s1ByTN1Tsk0s9zGVyGZ3YleJMxikKt3YRM~PEOE69d414aVlI8RxPRkwnHl10T51V5lpTRHANUkuGlt0UclXfMbNkv3aS2r50Ejyj3nbML48oVXXzuwswLnY5GQ0FAq-kPggPlDI-7WSAmozkVHscsO5RE1FUiYNy-TqhKBPE~hk6BDd70n9cMyDr~O2ac9IhoUg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL2000: "https://stagingcdn.permanent.org/00in-0000.thumb.w2000?t=1686306811&Expires=1686306811&Signature=Bay2a-H0hfxh91Tzdz3MmK6sZBuCkK0TTuu54nhQojMqbXzlFZ-Hxqchcixa0lweYW41v4iRJBymD49af92VMlFZYgDwmw8ER6P3iofgGI99y8nMaPGrMsZNV834p-xiZsgD53WLnuR7m5hEOy588-EBYqZjTp9pBCAKaPV-5IcjngvNOh6LcYmX~9kvWchG~NMamkmRLZ0mb5wGNtjny6ZCMcuCC5ta4hdro~NYhGCOqEnz9d35ofjdSqorxIB2gyX2mpIJsyYy9DoEIB5hFwQIgUP9DMZexEB3Bj1sArP5HJ54IKYwqsvXSy64EQMGHSlSMRiu7FAqvttaUhyINg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbDT: "2023-06-09T10:33:31", createdDT: "2021-01-27T19:48:08", updatedDT: "2022-07-07T07:45:44", status: ArchiveVOData.Status.ok) + let archiveVO = ArchiveVOData(childFolderVOS: nil, folderSizeVOS: nil, recordVOS: nil, accessRole: "access.role.owner", fullName: "test name", spaceTotal: nil, spaceLeft: nil, fileTotal: nil, fileLeft: nil, relationType: nil, homeCity: nil, homeState: nil, homeCountry: nil, itemVOS: nil, birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 1653, publicDT: "2021-01-27T19:48:08", archiveNbr: "00in-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: "00in-000v", type: "type.archive.person", thumbStatus: ArchiveVOData.Status.ok, imageRatio: nil, thumbnail256: nil, thumbURL200: "https://stagingcdn.permanent.org/00in-0000.thumb.w200?t=1686306811&Expires=1686306811&Signature=ZXjq08upvH73kyiLZJb-IYlSO4Jz6SaICjGBwHlL-UaQqpd1VAK6KWKnkIQtrLgsfhdkUhwa-TZT5tBSOhFDoeXfxOSLEEF19ml0W~rhyWjXpxhbMhqCL3s43lQ4p0uTeo4KuGXxx6-egFIZK2Z-5hzL52e5tpUJ6r5gENiiSL5r02ZapGmnKDN-UmF6vxaGLYrIAFZ5CpIuV6zCLjaKjLl-P2Ehp~LVTogJF9Tq7vibVTwYcagN4dnO9iDRp4u2alv0SceW2n8NavGaby1tnvQekVnqajbL0Utl1s3kUxiZ2V9VQmGrNRrZ4RRC8lB1xG8hDlb4GpUaQ0H86xiESg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL500: "https://stagingcdn.permanent.org/00in-0000.thumb.w500?t=1686306811&Expires=1686306811&Signature=e5W18KBakMSwIr5EHohYgHf3Nz23VA7eb6-x-3D1NH6Lsq-hxJyIHf9CTCmcksK-HR-CJKyrnIDjA2frUwpT274sPQ4fqVbXh~Od5i7Nh2gv9Gogs73z2QZ5a4Vrnuxhl6ncxWlVixs8AiUu~3ZgjfGQjmk2YODr8aIfp2siAg3SoN0NG1tHQ5AY7QfzrnXRCx55-~g3DaPcU8nuOfhwHAmlQzzvkortQIT2v2OP81~Kh8FW64go7Da~L~4BLPclq2xBcgl9Nk3NKMDIsCPVcVOOOTODz5yvwUbrhxZ7MmPHSxaGxLe8hvJqbrWP~hWgcouM-BLMvFupsRe1thYQhA__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL1000: "https://stagingcdn.permanent.org/00in-0000.thumb.w1000?t=1686306811&Expires=1686306811&Signature=Y72DJ9LSrpWQIy2UkUWGil6ZkKaOl2CDJMD9mhxKjl7uPgDpcb3h8KKnjI34~1GyRgsDbB~rE2-TjhO2y7zm2YMfhHEu7yggh8F8wpVBJnETi-O4R0sceWu3pgeSXkyQ5rW~I3mkGPRV8IyoC2s1ByTN1Tsk0s9zGVyGZ3YleJMxikKt3YRM~PEOE69d414aVlI8RxPRkwnHl10T51V5lpTRHANUkuGlt0UclXfMbNkv3aS2r50Ejyj3nbML48oVXXzuwswLnY5GQ0FAq-kPggPlDI-7WSAmozkVHscsO5RE1FUiYNy-TqhKBPE~hk6BDd70n9cMyDr~O2ac9IhoUg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL2000: "https://stagingcdn.permanent.org/00in-0000.thumb.w2000?t=1686306811&Expires=1686306811&Signature=Bay2a-H0hfxh91Tzdz3MmK6sZBuCkK0TTuu54nhQojMqbXzlFZ-Hxqchcixa0lweYW41v4iRJBymD49af92VMlFZYgDwmw8ER6P3iofgGI99y8nMaPGrMsZNV834p-xiZsgD53WLnuR7m5hEOy588-EBYqZjTp9pBCAKaPV-5IcjngvNOh6LcYmX~9kvWchG~NMamkmRLZ0mb5wGNtjny6ZCMcuCC5ta4hdro~NYhGCOqEnz9d35ofjdSqorxIB2gyX2mpIJsyYy9DoEIB5hFwQIgUP9DMZexEB3Bj1sArP5HJ54IKYwqsvXSy64EQMGHSlSMRiu7FAqvttaUhyINg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbDT: "2023-06-09T10:33:31", createdDT: "2021-01-27T19:48:08", updatedDT: "2022-07-07T07:45:44", status: ArchiveVOData.Status.ok) let token: String = "token" diff --git a/PermanentTests/ShareExtensionTests.swift b/PermanentTests/ShareExtensionTests.swift index dcf8ff0d..af16e7fc 100644 --- a/PermanentTests/ShareExtensionTests.swift +++ b/PermanentTests/ShareExtensionTests.swift @@ -13,9 +13,9 @@ import KeychainSwift class ShareExtensionTests: XCTestCase { var sut: ShareExtensionViewModel! - let archiveNegativeTests = ArchiveVOData(childFolderVOS: nil, folderSizeVOS: nil, recordVOS: nil, accessRole: nil, fullName: nil, spaceTotal: nil, spaceLeft: nil, fileTotal: nil, fileLeft: nil, relationType: nil, homeCity: nil, homeState: nil, homeCountry: nil, itemVOS: nil, birthDay: nil, company: nil, archiveVODescription: nil, archiveID: nil, publicDT: nil, archiveNbr: nil, view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil) + let archiveNegativeTests = ArchiveVOData(childFolderVOS: nil, folderSizeVOS: nil, recordVOS: nil, accessRole: nil, fullName: nil, spaceTotal: nil, spaceLeft: nil, fileTotal: nil, fileLeft: nil, relationType: nil, homeCity: nil, homeState: nil, homeCountry: nil, itemVOS: nil, birthDay: nil, company: nil, archiveVODescription: nil, archiveID: nil, publicDT: nil, archiveNbr: nil, view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil) - let archivePositiveTests = ArchiveVOData(childFolderVOS: nil, folderSizeVOS: nil, recordVOS: nil, accessRole: "access.role.owner", fullName: "test name", spaceTotal: nil, spaceLeft: nil, fileTotal: nil, fileLeft: nil, relationType: nil, homeCity: nil, homeState: nil, homeCountry: nil, itemVOS: nil, birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 1653, publicDT: "2021-01-27T19:48:08", archiveNbr: "00in-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: "00in-000v", type: "type.archive.person", thumbStatus: ArchiveVOData.Status.ok, imageRatio: nil, thumbURL200: "https://stagingcdn.permanent.org/00in-0000.thumb.w200?t=1686306811&Expires=1686306811&Signature=ZXjq08upvH73kyiLZJb-IYlSO4Jz6SaICjGBwHlL-UaQqpd1VAK6KWKnkIQtrLgsfhdkUhwa-TZT5tBSOhFDoeXfxOSLEEF19ml0W~rhyWjXpxhbMhqCL3s43lQ4p0uTeo4KuGXxx6-egFIZK2Z-5hzL52e5tpUJ6r5gENiiSL5r02ZapGmnKDN-UmF6vxaGLYrIAFZ5CpIuV6zCLjaKjLl-P2Ehp~LVTogJF9Tq7vibVTwYcagN4dnO9iDRp4u2alv0SceW2n8NavGaby1tnvQekVnqajbL0Utl1s3kUxiZ2V9VQmGrNRrZ4RRC8lB1xG8hDlb4GpUaQ0H86xiESg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL500: "https://stagingcdn.permanent.org/00in-0000.thumb.w500?t=1686306811&Expires=1686306811&Signature=e5W18KBakMSwIr5EHohYgHf3Nz23VA7eb6-x-3D1NH6Lsq-hxJyIHf9CTCmcksK-HR-CJKyrnIDjA2frUwpT274sPQ4fqVbXh~Od5i7Nh2gv9Gogs73z2QZ5a4Vrnuxhl6ncxWlVixs8AiUu~3ZgjfGQjmk2YODr8aIfp2siAg3SoN0NG1tHQ5AY7QfzrnXRCx55-~g3DaPcU8nuOfhwHAmlQzzvkortQIT2v2OP81~Kh8FW64go7Da~L~4BLPclq2xBcgl9Nk3NKMDIsCPVcVOOOTODz5yvwUbrhxZ7MmPHSxaGxLe8hvJqbrWP~hWgcouM-BLMvFupsRe1thYQhA__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL1000: "https://stagingcdn.permanent.org/00in-0000.thumb.w1000?t=1686306811&Expires=1686306811&Signature=Y72DJ9LSrpWQIy2UkUWGil6ZkKaOl2CDJMD9mhxKjl7uPgDpcb3h8KKnjI34~1GyRgsDbB~rE2-TjhO2y7zm2YMfhHEu7yggh8F8wpVBJnETi-O4R0sceWu3pgeSXkyQ5rW~I3mkGPRV8IyoC2s1ByTN1Tsk0s9zGVyGZ3YleJMxikKt3YRM~PEOE69d414aVlI8RxPRkwnHl10T51V5lpTRHANUkuGlt0UclXfMbNkv3aS2r50Ejyj3nbML48oVXXzuwswLnY5GQ0FAq-kPggPlDI-7WSAmozkVHscsO5RE1FUiYNy-TqhKBPE~hk6BDd70n9cMyDr~O2ac9IhoUg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL2000: "https://stagingcdn.permanent.org/00in-0000.thumb.w2000?t=1686306811&Expires=1686306811&Signature=Bay2a-H0hfxh91Tzdz3MmK6sZBuCkK0TTuu54nhQojMqbXzlFZ-Hxqchcixa0lweYW41v4iRJBymD49af92VMlFZYgDwmw8ER6P3iofgGI99y8nMaPGrMsZNV834p-xiZsgD53WLnuR7m5hEOy588-EBYqZjTp9pBCAKaPV-5IcjngvNOh6LcYmX~9kvWchG~NMamkmRLZ0mb5wGNtjny6ZCMcuCC5ta4hdro~NYhGCOqEnz9d35ofjdSqorxIB2gyX2mpIJsyYy9DoEIB5hFwQIgUP9DMZexEB3Bj1sArP5HJ54IKYwqsvXSy64EQMGHSlSMRiu7FAqvttaUhyINg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbDT: "2023-06-09T10:33:31", createdDT: "2021-01-27T19:48:08", updatedDT: "2022-07-07T07:45:44", status: ArchiveVOData.Status.ok) + let archivePositiveTests = ArchiveVOData(childFolderVOS: nil, folderSizeVOS: nil, recordVOS: nil, accessRole: "access.role.owner", fullName: "test name", spaceTotal: nil, spaceLeft: nil, fileTotal: nil, fileLeft: nil, relationType: nil, homeCity: nil, homeState: nil, homeCountry: nil, itemVOS: nil, birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 1653, publicDT: "2021-01-27T19:48:08", archiveNbr: "00in-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: "00in-000v", type: "type.archive.person", thumbStatus: ArchiveVOData.Status.ok, imageRatio: nil, thumbnail256: nil, thumbURL200: "https://stagingcdn.permanent.org/00in-0000.thumb.w200?t=1686306811&Expires=1686306811&Signature=ZXjq08upvH73kyiLZJb-IYlSO4Jz6SaICjGBwHlL-UaQqpd1VAK6KWKnkIQtrLgsfhdkUhwa-TZT5tBSOhFDoeXfxOSLEEF19ml0W~rhyWjXpxhbMhqCL3s43lQ4p0uTeo4KuGXxx6-egFIZK2Z-5hzL52e5tpUJ6r5gENiiSL5r02ZapGmnKDN-UmF6vxaGLYrIAFZ5CpIuV6zCLjaKjLl-P2Ehp~LVTogJF9Tq7vibVTwYcagN4dnO9iDRp4u2alv0SceW2n8NavGaby1tnvQekVnqajbL0Utl1s3kUxiZ2V9VQmGrNRrZ4RRC8lB1xG8hDlb4GpUaQ0H86xiESg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL500: "https://stagingcdn.permanent.org/00in-0000.thumb.w500?t=1686306811&Expires=1686306811&Signature=e5W18KBakMSwIr5EHohYgHf3Nz23VA7eb6-x-3D1NH6Lsq-hxJyIHf9CTCmcksK-HR-CJKyrnIDjA2frUwpT274sPQ4fqVbXh~Od5i7Nh2gv9Gogs73z2QZ5a4Vrnuxhl6ncxWlVixs8AiUu~3ZgjfGQjmk2YODr8aIfp2siAg3SoN0NG1tHQ5AY7QfzrnXRCx55-~g3DaPcU8nuOfhwHAmlQzzvkortQIT2v2OP81~Kh8FW64go7Da~L~4BLPclq2xBcgl9Nk3NKMDIsCPVcVOOOTODz5yvwUbrhxZ7MmPHSxaGxLe8hvJqbrWP~hWgcouM-BLMvFupsRe1thYQhA__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL1000: "https://stagingcdn.permanent.org/00in-0000.thumb.w1000?t=1686306811&Expires=1686306811&Signature=Y72DJ9LSrpWQIy2UkUWGil6ZkKaOl2CDJMD9mhxKjl7uPgDpcb3h8KKnjI34~1GyRgsDbB~rE2-TjhO2y7zm2YMfhHEu7yggh8F8wpVBJnETi-O4R0sceWu3pgeSXkyQ5rW~I3mkGPRV8IyoC2s1ByTN1Tsk0s9zGVyGZ3YleJMxikKt3YRM~PEOE69d414aVlI8RxPRkwnHl10T51V5lpTRHANUkuGlt0UclXfMbNkv3aS2r50Ejyj3nbML48oVXXzuwswLnY5GQ0FAq-kPggPlDI-7WSAmozkVHscsO5RE1FUiYNy-TqhKBPE~hk6BDd70n9cMyDr~O2ac9IhoUg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbURL2000: "https://stagingcdn.permanent.org/00in-0000.thumb.w2000?t=1686306811&Expires=1686306811&Signature=Bay2a-H0hfxh91Tzdz3MmK6sZBuCkK0TTuu54nhQojMqbXzlFZ-Hxqchcixa0lweYW41v4iRJBymD49af92VMlFZYgDwmw8ER6P3iofgGI99y8nMaPGrMsZNV834p-xiZsgD53WLnuR7m5hEOy588-EBYqZjTp9pBCAKaPV-5IcjngvNOh6LcYmX~9kvWchG~NMamkmRLZ0mb5wGNtjny6ZCMcuCC5ta4hdro~NYhGCOqEnz9d35ofjdSqorxIB2gyX2mpIJsyYy9DoEIB5hFwQIgUP9DMZexEB3Bj1sArP5HJ54IKYwqsvXSy64EQMGHSlSMRiu7FAqvttaUhyINg__&Key-Pair-Id=APKAJP2D34UGZ6IG443Q", thumbDT: "2023-06-09T10:33:31", createdDT: "2021-01-27T19:48:08", updatedDT: "2022-07-07T07:45:44", status: ArchiveVOData.Status.ok) let token: String = "token" diff --git a/PermanentTests/ShareItemViewModelTests.swift b/PermanentTests/ShareItemViewModelTests.swift index 57ae52d5..8ad14e52 100644 --- a/PermanentTests/ShareItemViewModelTests.swift +++ b/PermanentTests/ShareItemViewModelTests.swift @@ -43,7 +43,7 @@ final class ShareItemViewModelTests: XCTestCase { ) XCTAssertEqual(vm.fileName, fileModel.name, "File name should match") - XCTAssertEqual(vm.thumbnailURL, fileModel.thumbnailURL500, "Thumbnail URL should match") + XCTAssertEqual(vm.thumbnailURL, fileModel.preferredThumbnailURL, "Thumbnail URL should match") XCTAssertFalse(vm.isFolder, "Mock file should not be folder") } @@ -1357,3 +1357,230 @@ extension FileModel { ) } } + +final class ThumbnailSelectionTests: XCTestCase { + func testRecordPreferredThumbnailURL_UsesThumbnail256First() throws { + let record = try decode( + RecordVOData.self, + from: """ + { + "thumbnail256": "https://example.com/thumb256.jpg", + "thumbURL200": "https://example.com/thumb200.jpg", + "thumbURL500": "https://example.com/thumb500.jpg", + "thumbURL1000": "https://example.com/thumb1000.jpg", + "thumbURL2000": "https://example.com/thumb2000.jpg" + } + """ + ) + + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/thumb256.jpg") + } + + func testRecordPreferredThumbnailURL_FallsBackWhenThumbnail256Missing() throws { + let record = try decode( + RecordVOData.self, + from: """ + { + "thumbURL200": "https://example.com/thumb200.jpg", + "thumbURL500": "https://example.com/thumb500.jpg", + "thumbURL1000": "https://example.com/thumb1000.jpg", + "thumbURL2000": "https://example.com/thumb2000.jpg" + } + """ + ) + + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/thumb500.jpg") + } + + func testRecordPreferredThumbnailURL_UsesNextAvailableFallback() throws { + let record = try decode( + RecordVOData.self, + from: """ + { + "thumbURL1000": "https://example.com/thumb1000.jpg", + "thumbURL2000": "https://example.com/thumb2000.jpg" + } + """ + ) + + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/thumb1000.jpg") + } + + func testItemPreferredThumbnailURL_UsesThumbnail256First() throws { + let item = try decode( + ItemVO.self, + from: """ + { + "thumbnail256": "https://example.com/thumb256.jpg", + "thumbURL200": "https://example.com/thumb200.jpg", + "thumbURL500": "https://example.com/thumb500.jpg" + } + """ + ) + + XCTAssertEqual(item.preferredThumbnailURL, "https://example.com/thumb256.jpg") + } + + func testFolderPreferredThumbnailURL_FallsBackTo200When500Missing() throws { + let folder = try decode( + FolderVOData.self, + from: """ + { + "thumbURL200": "https://example.com/thumb200.jpg", + "thumbURL1000": "https://example.com/thumb1000.jpg" + } + """ + ) + + XCTAssertEqual(folder.preferredThumbnailURL, "https://example.com/thumb200.jpg") + } + + func testArchivePreferredThumbnailURL_UsesThumbnail256First() throws { + let archive = try decode( + ArchiveVOData.self, + from: """ + { + "thumbnail256": "https://example.com/thumb256.jpg", + "thumbURL500": "https://example.com/thumb500.jpg", + "thumbURL2000": "https://example.com/thumb2000.jpg" + } + """ + ) + + XCTAssertEqual(archive.preferredThumbnailURL, "https://example.com/thumb256.jpg") + } + + func testRecordFileModel_UsesThumbnail256ForPreferredThumbnail() throws { + let record = try decode( + RecordVOData.self, + from: """ + { + "displayName": "IMG_0111", + "displayDT": "2026-04-17T11:44:53", + "thumbnail256": "https://example.com/thumb256.jpg", + "thumbURL200": "https://example.com/thumb200.jpg", + "thumbURL500": "https://example.com/thumb500.jpg", + "type": "type.record.image", + "archiveId": 1, + "archiveNbr": "0fsv-009b", + "recordId": 2137129, + "parentFolderId": 180530, + "parentFolder_linkId": 1356908, + "folder_linkId": 2144172 + } + """ + ) + + let fileModel = FileModel(model: record, permissions: [.read], accessRole: .viewer) + + XCTAssertEqual(fileModel.thumbnailURL256, "https://example.com/thumb256.jpg") + XCTAssertEqual(fileModel.preferredThumbnailURL, "https://example.com/thumb256.jpg") + XCTAssertEqual(fileModel.thumbnailURL500, "https://example.com/thumb256.jpg") + } + + func testFolderFileModel_UsesThumbnail256ForPreferredThumbnail() throws { + let folder = try decode( + FolderVOData.self, + from: """ + { + "displayName": "Public", + "displayDT": "2026-04-17T11:44:53", + "thumbnail256": "https://example.com/thumb256.jpg", + "thumbURL200": "https://example.com/thumb200.jpg", + "thumbURL500": "https://example.com/thumb500.jpg", + "archiveId": 1, + "archiveNbr": "0fsv-0004", + "folderId": 180531, + "parentFolderId": 180528, + "parentFolder_linkId": 1356906, + "folder_linkId": 1356909 + } + """ + ) + + let fileModel = FileModel(model: folder) + + XCTAssertEqual(fileModel.thumbnailURL256, "https://example.com/thumb256.jpg") + XCTAssertEqual(fileModel.preferredThumbnailURL, "https://example.com/thumb256.jpg") + XCTAssertEqual(fileModel.thumbnailURL500, "https://example.com/thumb256.jpg") + } + + func testRecordV2PreferredThumbnailURL_UsesThumbnailUrls256WhenTopLevelThumbnail256Missing() throws { + let record = try decode( + RecordV2Data.self, + from: """ + { + "thumbnailUrls": { + "256": "https://example.com/thumb256.jpg", + "500": "https://example.com/thumb500-from-map.jpg" + }, + "thumbUrl200": "https://example.com/thumb200.jpg", + "thumbUrl500": "https://example.com/thumb500.jpg", + "thumbUrl1000": "https://example.com/thumb1000.jpg", + "thumbUrl2000": "https://example.com/thumb2000.jpg" + } + """ + ) + + XCTAssertEqual(record.resolvedThumbnail256, "https://example.com/thumb256.jpg") + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/thumb256.jpg") + } + + func testRecordV2PreferredThumbnailURL_PrefersTopLevelThumbnail256OverThumbnailUrls256() throws { + let record = try decode( + RecordV2Data.self, + from: """ + { + "thumbnail256": "https://example.com/top-level-256.jpg", + "thumbnailUrls": { + "256": "https://example.com/nested-256.jpg" + }, + "thumbUrl500": "https://example.com/thumb500.jpg" + } + """ + ) + + XCTAssertEqual(record.resolvedThumbnail256, "https://example.com/top-level-256.jpg") + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/top-level-256.jpg") + } + + func testRecordV2PreferredThumbnailURL_FallsBackToThumbUrl500WhenNo256IsAvailable() throws { + let record = try decode( + RecordV2Data.self, + from: """ + { + "thumbUrl200": "https://example.com/thumb200.jpg", + "thumbUrl500": "https://example.com/thumb500.jpg", + "thumbUrl1000": "https://example.com/thumb1000.jpg" + } + """ + ) + + XCTAssertNil(record.resolvedThumbnail256) + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/thumb500.jpg") + } + + func testRecordShareArchiveV2PreferredThumbnailURL_UsesThumbnailUrls256() throws { + let archive = try decode( + RecordShareArchiveV2.self, + from: """ + { + "id": "21588", + "thumbnailUrls": { + "256": "https://example.com/thumb256.jpg" + }, + "thumbUrl500": "https://example.com/thumb500.jpg", + "thumbUrl200": "https://example.com/thumb200.jpg" + } + """ + ) + + XCTAssertEqual(archive.resolvedThumbnail256, "https://example.com/thumb256.jpg") + XCTAssertEqual(archive.preferredThumbnailURL, "https://example.com/thumb256.jpg") + } +} + +private func decode(_ type: T.Type, from json: String) throws -> T { + let data = Data(json.utf8) + return try JSONDecoder().decode(T.self, from: data) +} diff --git a/PermanentTests/SharePreviewSwiftUIViewModelTests.swift b/PermanentTests/SharePreviewSwiftUIViewModelTests.swift index 1dc3e15f..3996f361 100644 --- a/PermanentTests/SharePreviewSwiftUIViewModelTests.swift +++ b/PermanentTests/SharePreviewSwiftUIViewModelTests.swift @@ -115,7 +115,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { itemVOS: nil, birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 9999, publicDT: nil, archiveNbr: "9999-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, - thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, + thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil ) @@ -158,7 +158,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { itemVOS: nil, birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 9999, publicDT: nil, archiveNbr: "9999-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, - thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, + thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil ) @@ -834,7 +834,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 9999, publicDT: nil, archiveNbr: "9999-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, - thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: nil, + thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil ) @@ -881,7 +881,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 9999, publicDT: nil, archiveNbr: "9999-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, - thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: nil, + thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil ) @@ -1199,7 +1199,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 9999, publicDT: nil, archiveNbr: "9999-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, - thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: nil, + thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil ) @@ -1362,7 +1362,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 9999, publicDT: nil, archiveNbr: "9999-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, - thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: nil, + thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil ) @@ -1451,7 +1451,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 10629, publicDT: nil, archiveNbr: "07cm-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, - thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: nil, + thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: .ok ) @@ -1483,7 +1483,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 10272, publicDT: nil, archiveNbr: "072p-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, - thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: nil, + thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: .ok ) @@ -1514,7 +1514,7 @@ final class SharePreviewSwiftUIViewModelTests: XCTestCase { birthDay: nil, company: nil, archiveVODescription: nil, archiveID: 99999, publicDT: nil, archiveNbr: "9999-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, - thumbStatus: nil, imageRatio: nil, thumbURL200: nil, thumbURL500: nil, + thumbStatus: nil, imageRatio: nil, thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: .ok ) @@ -1814,6 +1814,7 @@ private struct RestrictedShareApprovedRepo: SharePreviewRepositoryProtocol { type: data.folderData?.type, thumbStatus: data.folderData?.thumbStatus, imageRatio: data.folderData?.imageRatio, + thumbnail256: data.folderData?.thumbnail256, thumbURL200: data.folderData?.thumbURL200, thumbURL500: data.folderData?.thumbURL500, thumbURL1000: data.folderData?.thumbURL1000, diff --git a/PermanentTests/SharePreviewViewModelTests.swift b/PermanentTests/SharePreviewViewModelTests.swift index 29fff72f..aad3e934 100644 --- a/PermanentTests/SharePreviewViewModelTests.swift +++ b/PermanentTests/SharePreviewViewModelTests.swift @@ -290,6 +290,7 @@ final class SharePreviewViewModelTests: XCTestCase { archiveID: 123, publicDT: nil, archiveNbr: "00te-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, + thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: nil ) @@ -344,6 +345,7 @@ final class SharePreviewViewModelTests: XCTestCase { archiveID: id, publicDT: nil, archiveNbr: "0001-0000", view: nil, viewProperty: nil, archiveVOPublic: nil, vaultKey: nil, thumbArchiveNbr: nil, type: nil, thumbStatus: nil, imageRatio: nil, + thumbnail256: nil, thumbURL200: nil, thumbURL500: nil, thumbURL1000: nil, thumbURL2000: nil, thumbDT: nil, createdDT: nil, updatedDT: nil, status: .ok ) From 582a2017dcff4f7050f571c39b1ff62abccbf842 Mon Sep 17 00:00:00 2001 From: Lucian Cerbu Date: Tue, 21 Apr 2026 18:32:23 +0300 Subject: [PATCH 2/3] Updated marketing version to 1.14.2 --- Permanent.xcodeproj/project.pbxproj | 48 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Permanent.xcodeproj/project.pbxproj b/Permanent.xcodeproj/project.pbxproj index 13fc8c72..e95c6010 100644 --- a/Permanent.xcodeproj/project.pbxproj +++ b/Permanent.xcodeproj/project.pbxproj @@ -6014,7 +6014,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -6072,7 +6072,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -6101,7 +6101,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS"; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6134,7 +6134,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -6158,7 +6158,7 @@ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive.PermanentUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -6183,7 +6183,7 @@ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive.PermanentUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -6208,7 +6208,7 @@ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive.PermanentUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -6233,7 +6233,7 @@ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive.PermanentUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -6264,7 +6264,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = com.vsp.PermanentTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -6294,7 +6294,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = com.vsp.PermanentTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -6324,7 +6324,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = com.vsp.PermanentTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -6354,7 +6354,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = com.vsp.PermanentTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -6390,7 +6390,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6428,7 +6428,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.permanent.staging.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6466,7 +6466,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6504,7 +6504,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.permanent.staging.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6573,7 +6573,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -6602,7 +6602,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; OTHER_SWIFT_FLAGS = "-D COCOAPODS -DSTAGING_ENVIRONMENT"; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.permanent.staging; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6663,7 +6663,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -6692,7 +6692,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; OTHER_SWIFT_FLAGS = "-D COCOAPODS -DSTAGING_ENVIRONMENT"; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.permanent.staging; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6722,7 +6722,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive.PushExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -6751,7 +6751,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.permanent.staging.PushExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -6780,7 +6780,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.PermanentArchive.PushExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -6809,7 +6809,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.14.1; + MARKETING_VERSION = 1.14.2; PRODUCT_BUNDLE_IDENTIFIER = org.permanent.permanent.staging.PushExtension; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 73c2b3bf561b44746a66de503ada38ff8cfc95cc Mon Sep 17 00:00:00 2001 From: Lucian Cerbu Date: Tue, 21 Apr 2026 19:49:37 +0300 Subject: [PATCH 3/3] Add thumbnail caching and fix empty string URL handling Implement SDWebImage two-stage progressive loading (256px thumbnail, then full-res) with cache configuration (300MB disk, 50MB memory) and cache clearing on logout. Fix thumbnail disappearing in share management when V2 API returns empty strings instead of null. Added nonEmpty() filtering to all preferredThumbnailURL properties across V1 and V2 models, plus guards in ShareItemViewModel and FileModel. Added 9 unit tests for empty string handling. --- Permanent/App/AppDelegate.swift | 8 + .../Managers/AuthenticationManager.swift | 5 + Permanent/Common/Models/Data/ArchiveVO.swift | 7 +- .../Common/Models/Data/Folder/FolderVO.swift | 7 +- .../Models/Data/Folder/MinFolderVO.swift | 7 +- Permanent/Common/Models/Data/ItemVO.swift | 7 +- .../Common/Models/Data/ParentFolderVO.swift | 7 +- .../Common/Models/Data/RecordV2Models.swift | 18 +- Permanent/Common/Models/Data/RecordVO.swift | 7 +- .../FileDetailsTopCollectionViewCell.swift | 38 +++- .../FilePreviewViewController.swift | 124 +++++++++--- .../ViewModels/ShareItemViewModel.swift | 14 +- .../ViewModels/ViewModel/FileModel.swift | 4 +- PermanentTests/ShareItemViewModelTests.swift | 181 ++++++++++++++++++ 14 files changed, 390 insertions(+), 44 deletions(-) diff --git a/Permanent/App/AppDelegate.swift b/Permanent/App/AppDelegate.swift index b1138bc9..7e2b9cc3 100644 --- a/Permanent/App/AppDelegate.swift +++ b/Permanent/App/AppDelegate.swift @@ -13,6 +13,7 @@ import GoogleMaps import StripeApplePay import SwiftUI import KeychainSwift +import SDWebImage @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -29,6 +30,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { initFirebase() initNotifications() configureLogging() + configureImageCache() StripeAPI.defaultPublishableKey = stripeServiceInfo.publishableKey @@ -285,6 +287,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #endif } + fileprivate func configureImageCache() { + let cache = SDImageCache.shared + cache.config.maxDiskSize = 300 * 1024 * 1024 // 300 MB disk cap + cache.config.maxMemoryCost = 50 * 1024 * 1024 // 50 MB memory cap + } + fileprivate func initNotifications() { UNUserNotificationCenter.current().delegate = self diff --git a/Permanent/Common/Managers/AuthenticationManager.swift b/Permanent/Common/Managers/AuthenticationManager.swift index 16824bc8..99bc72c4 100644 --- a/Permanent/Common/Managers/AuthenticationManager.swift +++ b/Permanent/Common/Managers/AuthenticationManager.swift @@ -8,6 +8,7 @@ import UIKit import KeychainSwift import FirebaseMessaging +import SDWebImage class AuthenticationManager { static let shared = AuthenticationManager() @@ -212,6 +213,10 @@ class AuthenticationManager { keychainHandler.clearSession() UserDefaults.standard.set(false, forKey: Constants.Keys.StorageKeys.memberChecklistWasShown) + // Clear cached images to prevent data leaking between accounts + SDImageCache.shared.clearMemory() + SDImageCache.shared.clearDisk() + Messaging.messaging().deleteFCMToken(forSenderID: googleServiceInfo.gcmSenderId) { _ in } } diff --git a/Permanent/Common/Models/Data/ArchiveVO.swift b/Permanent/Common/Models/Data/ArchiveVO.swift index 287cc3cf..6a413f02 100644 --- a/Permanent/Common/Models/Data/ArchiveVO.swift +++ b/Permanent/Common/Models/Data/ArchiveVO.swift @@ -70,8 +70,13 @@ struct ArchiveVOData: Model { } extension ArchiveVOData { + private func nonEmpty(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return value + } + var preferredThumbnailURL: String? { - thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + nonEmpty(thumbnail256) ?? nonEmpty(thumbURL500) ?? nonEmpty(thumbURL200) ?? nonEmpty(thumbURL1000) ?? nonEmpty(thumbURL2000) } } diff --git a/Permanent/Common/Models/Data/Folder/FolderVO.swift b/Permanent/Common/Models/Data/Folder/FolderVO.swift index 94df3b0b..ee5a636d 100644 --- a/Permanent/Common/Models/Data/Folder/FolderVO.swift +++ b/Permanent/Common/Models/Data/Folder/FolderVO.swift @@ -105,7 +105,12 @@ struct FolderVOData: Model { } extension FolderVOData { + private func nonEmpty(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return value + } + var preferredThumbnailURL: String? { - thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + nonEmpty(thumbnail256) ?? nonEmpty(thumbURL500) ?? nonEmpty(thumbURL200) ?? nonEmpty(thumbURL1000) ?? nonEmpty(thumbURL2000) } } diff --git a/Permanent/Common/Models/Data/Folder/MinFolderVO.swift b/Permanent/Common/Models/Data/Folder/MinFolderVO.swift index f1a43ea7..18053f51 100644 --- a/Permanent/Common/Models/Data/Folder/MinFolderVO.swift +++ b/Permanent/Common/Models/Data/Folder/MinFolderVO.swift @@ -98,7 +98,12 @@ struct MinFolderVO: Codable { } extension MinFolderVO { + private func nonEmpty(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return value + } + var preferredThumbnailURL: String? { - thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + nonEmpty(thumbnail256) ?? nonEmpty(thumbURL500) ?? nonEmpty(thumbURL200) ?? nonEmpty(thumbURL1000) ?? nonEmpty(thumbURL2000) } } diff --git a/Permanent/Common/Models/Data/ItemVO.swift b/Permanent/Common/Models/Data/ItemVO.swift index 631025cd..edd9effe 100644 --- a/Permanent/Common/Models/Data/ItemVO.swift +++ b/Permanent/Common/Models/Data/ItemVO.swift @@ -113,7 +113,12 @@ struct ItemVO: Model { } extension ItemVO { + private func nonEmpty(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return value + } + var preferredThumbnailURL: String? { - thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + nonEmpty(thumbnail256) ?? nonEmpty(thumbURL500) ?? nonEmpty(thumbURL200) ?? nonEmpty(thumbURL1000) ?? nonEmpty(thumbURL2000) } } diff --git a/Permanent/Common/Models/Data/ParentFolderVO.swift b/Permanent/Common/Models/Data/ParentFolderVO.swift index a80db7bd..cb1dac9e 100644 --- a/Permanent/Common/Models/Data/ParentFolderVO.swift +++ b/Permanent/Common/Models/Data/ParentFolderVO.swift @@ -94,7 +94,12 @@ struct ParentFolderVO: Model { } extension ParentFolderVO { + private func nonEmpty(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return value + } + var preferredThumbnailURL: String? { - thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + nonEmpty(thumbnail256) ?? nonEmpty(thumbURL500) ?? nonEmpty(thumbURL200) ?? nonEmpty(thumbURL1000) ?? nonEmpty(thumbURL2000) } } diff --git a/Permanent/Common/Models/Data/RecordV2Models.swift b/Permanent/Common/Models/Data/RecordV2Models.swift index fc849621..f81600ff 100644 --- a/Permanent/Common/Models/Data/RecordV2Models.swift +++ b/Permanent/Common/Models/Data/RecordV2Models.swift @@ -54,12 +54,17 @@ struct RecordV2Data: Model { } extension RecordV2Data { + private func nonEmpty(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return value + } + var resolvedThumbnail256: String? { - thumbnail256 ?? thumbnailUrls?.url256 + nonEmpty(thumbnail256) ?? nonEmpty(thumbnailUrls?.url256) } var preferredThumbnailURL: String? { - resolvedThumbnail256 ?? thumbUrl500 ?? thumbUrl200 ?? thumbUrl1000 ?? thumbUrl2000 + resolvedThumbnail256 ?? nonEmpty(thumbUrl500) ?? nonEmpty(thumbUrl200) ?? nonEmpty(thumbUrl1000) ?? nonEmpty(thumbUrl2000) } } @@ -120,12 +125,17 @@ struct RecordShareArchiveV2: Model { } extension RecordShareArchiveV2 { + private func nonEmpty(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return value + } + var resolvedThumbnail256: String? { - thumbnail256 ?? thumbnailUrls?.url256 + nonEmpty(thumbnail256) ?? nonEmpty(thumbnailUrls?.url256) } var preferredThumbnailURL: String? { - resolvedThumbnail256 ?? thumbUrl500 ?? thumbUrl200 ?? thumbUrl1000 ?? thumbUrl2000 + resolvedThumbnail256 ?? nonEmpty(thumbUrl500) ?? nonEmpty(thumbUrl200) ?? nonEmpty(thumbUrl1000) ?? nonEmpty(thumbUrl2000) } } diff --git a/Permanent/Common/Models/Data/RecordVO.swift b/Permanent/Common/Models/Data/RecordVO.swift index e81897a9..77f322a7 100644 --- a/Permanent/Common/Models/Data/RecordVO.swift +++ b/Permanent/Common/Models/Data/RecordVO.swift @@ -102,7 +102,12 @@ struct RecordVOData: Model { } extension RecordVOData { + private func nonEmpty(_ value: String?) -> String? { + guard let value = value, !value.isEmpty else { return nil } + return value + } + var preferredThumbnailURL: String? { - thumbnail256 ?? thumbURL500 ?? thumbURL200 ?? thumbURL1000 ?? thumbURL2000 + nonEmpty(thumbnail256) ?? nonEmpty(thumbURL500) ?? nonEmpty(thumbURL200) ?? nonEmpty(thumbURL1000) ?? nonEmpty(thumbURL2000) } } diff --git a/Permanent/Modules/FileOperations/Cells/FileDetailsTopCollectionViewCell.swift b/Permanent/Modules/FileOperations/Cells/FileDetailsTopCollectionViewCell.swift index e9fc30b5..262a724a 100644 --- a/Permanent/Modules/FileOperations/Cells/FileDetailsTopCollectionViewCell.swift +++ b/Permanent/Modules/FileOperations/Cells/FileDetailsTopCollectionViewCell.swift @@ -6,6 +6,7 @@ // import UIKit +import SDWebImage class FileDetailsTopCollectionViewCell: FileDetailsBaseCollectionViewCell { @@ -24,11 +25,40 @@ class FileDetailsTopCollectionViewCell: FileDetailsBaseCollectionViewCell { activityIndicator.startAnimating() imageView.image = nil - let urlString = viewModel.file.preferredThumbnailURL ?? viewModel.fileThumbnailURL() ?? "" - guard let url = URL(string: urlString) else { return } - imageView.sd_setImage(with: url) { image, error, cacheType, url in - self.activityIndicator.stopAnimating() + // Stage 1: Load the 256px thumbnail quickly + let thumbnailURLString = viewModel.file.preferredThumbnailURL ?? viewModel.fileThumbnailURL() ?? "" + guard let thumbnailURL = URL(string: thumbnailURLString) else { return } + + // Determine the full-res download URL if available + let fullResURLString = viewModel.fileVO()?.downloadURL + let fullResURL = fullResURLString.flatMap { URL(string: $0) } + + imageView.sd_setImage(with: thumbnailURL) { [weak self] _, error, _, _ in + guard let self = self else { return } + + if let fullResURL = fullResURL, fullResURL != thumbnailURL { + // Stage 2: Upgrade to full-res from download URL + self.imageView.sd_setImage( + with: fullResURL, + placeholderImage: self.imageView.image, + options: [.avoidAutoSetImage, .retryFailed], + progress: nil + ) { [weak self] image, _, cacheType, _ in + guard let self = self, let image = image else { return } + self.activityIndicator.stopAnimating() + + if cacheType == .memory { + self.imageView.image = image + } else { + UIView.transition(with: self.imageView, duration: 0.3, options: .transitionCrossDissolve) { + self.imageView.image = image + } + } + } + } else { + self.activityIndicator.stopAnimating() + } } } diff --git a/Permanent/Modules/FileOperations/ViewController/FilePreviewViewController.swift b/Permanent/Modules/FileOperations/ViewController/FilePreviewViewController.swift index d6a8cab0..03463330 100644 --- a/Permanent/Modules/FileOperations/ViewController/FilePreviewViewController.swift +++ b/Permanent/Modules/FileOperations/ViewController/FilePreviewViewController.swift @@ -10,6 +10,7 @@ import WebKit import AVKit import PDFKit import SwiftUI +import SDWebImage class FilePreviewViewController: BaseViewController { override var supportedInterfaceOrientations: UIInterfaceOrientationMask { @@ -70,6 +71,8 @@ class FilePreviewViewController: BaseViewController { styleNavBar() } + var imagePreviewVC: ImagePreviewViewController? + func loadVM() { guard recordLoaded == false else { return } @@ -83,8 +86,8 @@ class FilePreviewViewController: BaseViewController { if viewModel == nil || viewModel?.recordVO == nil { viewModel = FilePreviewViewModel(file: file) - if file.type == .image, let url = URL(string: file.preferredThumbnailURL) { - loadImage(withURL: url) + if file.type == .image, let thumbnailURLString = file.preferredThumbnailURL { + loadThumbnailPreview(urlString: thumbnailURLString) } viewModel?.getRecord(file: file, then: { [weak self] record in @@ -98,8 +101,8 @@ class FilePreviewViewController: BaseViewController { self?.retryButton.isHidden = false } }) - } else if file.type == .image, let url = URL(string: file.preferredThumbnailURL) { - loadImage(withURL: url) + } else if file.type == .image, let thumbnailURLString = file.preferredThumbnailURL { + loadThumbnailPreview(urlString: thumbnailURLString) } else { loadRecord() } @@ -168,7 +171,9 @@ class FilePreviewViewController: BaseViewController { let contentType = fileVO.contentType { switch fileType { case FileType.image: - if let url = URL(string: self.viewModel?.fileThumbnailURL()) { + // Use download URL for full-res; fall back to thumbnail URL + let fullResURL = fileVO.downloadURL ?? self.viewModel?.fileThumbnailURL() + if let urlString = fullResURL, let url = URL(string: urlString) { self.loadImage(withURL: url) } @@ -189,9 +194,8 @@ class FilePreviewViewController: BaseViewController { let downloadURL = URL(string: downloadURLString) { switch fileType { case FileType.image: - if let url = URL(string: self.viewModel?.fileThumbnailURL()) { - self.loadImage(withURL: url) - } + // Use download URL for full-resolution image + self.loadImage(withURL: downloadURL) case FileType.video: self.loadVideo(withURL: downloadURL, contentType: contentType) @@ -215,28 +219,102 @@ class FilePreviewViewController: BaseViewController { } } - func loadImage(withURL url: URL) { - let imagePreviewVC = ImagePreviewViewController() - imagePreviewVC.delegate = self - addChild(imagePreviewVC) - imagePreviewVC.view.frame = view.bounds - imagePreviewVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - // Insert view under the spinner - view.insertSubview(imagePreviewVC.view, at: 0) - imagePreviewVC.didMove(toParent: self) - imagePreviewVC.imageView.sd_setImage(with: url) { _, error, _, _ in + /// Loads the 256px thumbnail into the zoomable image preview as a quick placeholder. + private func loadThumbnailPreview(urlString: String) { + guard let url = URL(string: urlString) else { return } + + let previewVC = ImagePreviewViewController() + previewVC.delegate = self + self.imagePreviewVC = previewVC + + addChild(previewVC) + previewVC.view.frame = view.bounds + previewVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.insertSubview(previewVC.view, at: 0) + previewVC.didMove(toParent: self) + + previewVC.imageView.sd_setImage(with: url) { [weak self] _, error, _, _ in + guard let self = self else { return } self.activityIndicator.stopAnimating() self.thumbnailImageView.isHidden = true if error != nil { self.errorLabel.isHidden = false self.retryButton.isHidden = false - - imagePreviewVC.view.removeFromSuperview() - imagePreviewVC.removeFromParent() - imagePreviewVC.didMove(toParent: nil) + self.removeImagePreviewVC() + } else { + previewVC.newImageLoaded() + } + } + } + + /// Upgrades the existing image preview from thumbnail to full-resolution using the download URL. + /// SDWebImage caches the full-res image, so subsequent opens load instantly from disk. + private func upgradeToFullResolution(urlString: String) { + guard let url = URL(string: urlString), + let previewVC = self.imagePreviewVC else { return } + + // Check if SDWebImage already has this URL cached (from a previous open) + let cachedFromMemory = SDImageCache.shared.imageFromMemoryCache(forKey: urlString) != nil + + previewVC.imageView.sd_setImage( + with: url, + placeholderImage: previewVC.imageView.image, + options: [.avoidAutoSetImage, .retryFailed], + progress: nil + ) { [weak previewVC] image, _, cacheType, _ in + guard let previewVC = previewVC, let image = image else { return } + + if cachedFromMemory || cacheType == .memory { + // Cached in memory — swap immediately, no animation needed + previewVC.imageView.image = image + previewVC.newImageLoaded() } else { - imagePreviewVC.newImageLoaded() + // Downloaded or loaded from disk — crossfade for smooth transition + UIView.transition(with: previewVC.imageView, duration: 0.3, options: .transitionCrossDissolve) { + previewVC.imageView.image = image + } completion: { _ in + previewVC.newImageLoaded() + } + } + } + } + + private func removeImagePreviewVC() { + imagePreviewVC?.view.removeFromSuperview() + imagePreviewVC?.removeFromParent() + imagePreviewVC?.didMove(toParent: nil) + imagePreviewVC = nil + } + + func loadImage(withURL url: URL) { + if imagePreviewVC != nil { + // Image preview already showing thumbnail — upgrade to full-res + upgradeToFullResolution(urlString: url.absoluteString) + } else { + // No preview yet (e.g. record already loaded) — load directly + let previewVC = ImagePreviewViewController() + previewVC.delegate = self + self.imagePreviewVC = previewVC + + addChild(previewVC) + previewVC.view.frame = view.bounds + previewVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.insertSubview(previewVC.view, at: 0) + previewVC.didMove(toParent: self) + + previewVC.imageView.sd_setImage(with: url) { [weak self] _, error, _, _ in + guard let self = self else { return } + self.activityIndicator.stopAnimating() + self.thumbnailImageView.isHidden = true + + if error != nil { + self.errorLabel.isHidden = false + self.retryButton.isHidden = false + self.removeImagePreviewVC() + } else { + previewVC.newImageLoaded() + } } } } diff --git a/Permanent/Modules/Shares/SwiftUIViews/ViewModels/ShareItemViewModel.swift b/Permanent/Modules/Shares/SwiftUIViews/ViewModels/ShareItemViewModel.swift index aeab0bf5..d182717f 100644 --- a/Permanent/Modules/Shares/SwiftUIViews/ViewModels/ShareItemViewModel.swift +++ b/Permanent/Modules/Shares/SwiftUIViews/ViewModels/ShareItemViewModel.swift @@ -191,10 +191,11 @@ class ShareItemViewModel: ObservableObject { var thumbnailURL: String? { // Use V2 record thumbnail if available, otherwise use fileModel if let v2Data = shareLinkV2Data, v2Data.itemType == "record", - let recordThumb = recordV2ThumbnailURL { + let recordThumb = recordV2ThumbnailURL, !recordThumb.isEmpty { return recordThumb } - return fileModel.preferredThumbnailURL + guard let url = fileModel.preferredThumbnailURL, !url.isEmpty else { return nil } + return url } var isFolder: Bool { @@ -1220,16 +1221,17 @@ class ShareItemViewModel: ObservableObject { ] // Add thumbnail URLs in priority order (use highest quality available) - if let thumbUrl2000 = archiveData.thumbUrl2000 { + // Filter out empty strings that V2 API may return instead of null + if let thumbUrl2000 = archiveData.thumbUrl2000, !thumbUrl2000.isEmpty { archiveDict["thumbURL2000"] = thumbUrl2000 } - if let thumbUrl1000 = archiveData.thumbUrl1000 { + if let thumbUrl1000 = archiveData.thumbUrl1000, !thumbUrl1000.isEmpty { archiveDict["thumbURL1000"] = thumbUrl1000 } - if let thumbUrl500 = archiveData.thumbUrl500 { + if let thumbUrl500 = archiveData.thumbUrl500, !thumbUrl500.isEmpty { archiveDict["thumbURL500"] = thumbUrl500 } - if let thumbUrl200 = archiveData.thumbUrl200 { + if let thumbUrl200 = archiveData.thumbUrl200, !thumbUrl200.isEmpty { archiveDict["thumbURL200"] = thumbUrl200 } diff --git a/Permanent/ViewModels/ViewModel/FileModel.swift b/Permanent/ViewModels/ViewModel/FileModel.swift index 102e765a..1fa26054 100644 --- a/Permanent/ViewModels/ViewModel/FileModel.swift +++ b/Permanent/ViewModels/ViewModel/FileModel.swift @@ -305,6 +305,8 @@ struct FileModel: Equatable, Codable { } var preferredThumbnailURL: String? { - thumbnailURL256 ?? thumbnailURL500 ?? thumbnailURL ?? thumbnailURL1000 ?? thumbnailURL2000 + [thumbnailURL256, thumbnailURL500, thumbnailURL, thumbnailURL1000, thumbnailURL2000] + .compactMap { $0 } + .first { !$0.isEmpty } } } diff --git a/PermanentTests/ShareItemViewModelTests.swift b/PermanentTests/ShareItemViewModelTests.swift index 8ad14e52..4555a924 100644 --- a/PermanentTests/ShareItemViewModelTests.swift +++ b/PermanentTests/ShareItemViewModelTests.swift @@ -1578,6 +1578,187 @@ final class ThumbnailSelectionTests: XCTestCase { XCTAssertEqual(archive.resolvedThumbnail256, "https://example.com/thumb256.jpg") XCTAssertEqual(archive.preferredThumbnailURL, "https://example.com/thumb256.jpg") } + + // MARK: - Empty String Handling Tests + + func testRecordV2PreferredThumbnailURL_SkipsEmptyThumbnail256() throws { + let record = try decode( + RecordV2Data.self, + from: """ + { + "thumbnail256": "", + "thumbUrl500": "https://example.com/thumb500.jpg", + "thumbUrl200": "https://example.com/thumb200.jpg" + } + """ + ) + + XCTAssertNil(record.resolvedThumbnail256, "Empty thumbnail256 should be treated as nil") + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/thumb500.jpg", + "Should fall through to thumbUrl500 when thumbnail256 is empty") + } + + func testRecordV2PreferredThumbnailURL_SkipsEmptyThumbnailUrls256() throws { + let record = try decode( + RecordV2Data.self, + from: """ + { + "thumbnailUrls": { + "256": "" + }, + "thumbUrl500": "https://example.com/thumb500.jpg" + } + """ + ) + + XCTAssertNil(record.resolvedThumbnail256, "Empty thumbnailUrls.256 should be treated as nil") + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/thumb500.jpg") + } + + func testRecordV2PreferredThumbnailURL_SkipsAllEmptyStrings() throws { + let record = try decode( + RecordV2Data.self, + from: """ + { + "thumbnail256": "", + "thumbUrl500": "", + "thumbUrl200": "", + "thumbUrl1000": "", + "thumbUrl2000": "https://example.com/thumb2000.jpg" + } + """ + ) + + XCTAssertEqual(record.preferredThumbnailURL, "https://example.com/thumb2000.jpg", + "Should skip all empty strings and use the first non-empty URL") + } + + func testRecordV2PreferredThumbnailURL_ReturnsNilWhenAllEmpty() throws { + let record = try decode( + RecordV2Data.self, + from: """ + { + "thumbnail256": "", + "thumbUrl500": "", + "thumbUrl200": "", + "thumbUrl1000": "", + "thumbUrl2000": "" + } + """ + ) + + XCTAssertNil(record.preferredThumbnailURL, + "Should return nil when all thumbnail URLs are empty strings") + } + + func testRecordShareArchiveV2PreferredThumbnailURL_SkipsEmptyStrings() throws { + let archive = try decode( + RecordShareArchiveV2.self, + from: """ + { + "id": "21588", + "thumbnail256": "", + "thumbUrl500": "", + "thumbUrl200": "https://example.com/thumb200.jpg" + } + """ + ) + + XCTAssertNil(archive.resolvedThumbnail256, "Empty thumbnail256 should be treated as nil") + XCTAssertEqual(archive.preferredThumbnailURL, "https://example.com/thumb200.jpg", + "Should skip empty strings and use first valid URL") + } + + func testArchiveVOPreferredThumbnailURL_SkipsEmptyStrings() throws { + let archive = try decode( + ArchiveVOData.self, + from: """ + { + "thumbnail256": "", + "thumbURL500": "", + "thumbURL200": "https://example.com/thumb200.jpg" + } + """ + ) + + XCTAssertEqual(archive.preferredThumbnailURL, "https://example.com/thumb200.jpg", + "Should skip empty strings and use first valid URL") + } + + func testFileModelPreferredThumbnailURL_SkipsEmptyStrings() throws { + let record = try decode( + RecordVOData.self, + from: """ + { + "displayName": "Test.jpg", + "thumbnail256": "", + "thumbURL500": "", + "thumbURL200": "", + "thumbURL1000": "", + "thumbURL2000": "https://example.com/thumb2000.jpg", + "type": "type.record.image", + "archiveId": 1, + "archiveNbr": "0001-0000", + "recordId": 1, + "parentFolderId": 1, + "parentFolder_linkId": 1, + "folder_linkId": 1 + } + """ + ) + + let fileModel = FileModel(model: record, permissions: [.read], accessRole: .viewer) + + XCTAssertEqual(fileModel.preferredThumbnailURL, "https://example.com/thumb2000.jpg", + "Should skip empty strings and use first valid URL") + } + + func testFileModelPreferredThumbnailURL_ReturnsNilWhenAllEmpty() throws { + let record = try decode( + RecordVOData.self, + from: """ + { + "displayName": "Test.jpg", + "thumbnail256": "", + "thumbURL500": "", + "thumbURL200": "", + "thumbURL1000": "", + "thumbURL2000": "", + "type": "type.record.image", + "archiveId": 1, + "archiveNbr": "0001-0000", + "recordId": 1, + "parentFolderId": 1, + "parentFolder_linkId": 1, + "folder_linkId": 1 + } + """ + ) + + let fileModel = FileModel(model: record, permissions: [.read], accessRole: .viewer) + + XCTAssertNil(fileModel.preferredThumbnailURL, + "Should return nil when all thumbnail URLs are empty") + } + + @MainActor + func testShareItemViewModel_ThumbnailURL_WithNoThumbnails() { + let fileModel = FileModel( + name: "Test.jpg", + recordId: 1, + folderLinkId: 1, + archiveNbr: "0001-0000", + type: "type.record.image", + permissions: [.read] + ) + + let vm = ShareItemViewModel( + fileModel: fileModel, + shareManagementRepository: MockShareManagementRepository() + ) + + XCTAssertNil(vm.thumbnailURL, "Should return nil when fileModel has no valid thumbnail URLs") + } } private func decode(_ type: T.Type, from json: String) throws -> T {