diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0bcadf5202f5..b2e8fe9d3999 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,6 +2,8 @@ ----- * [*] Fix button overlap in the "Post Published" sheet on small devices [#23210] * [*] [internal] Incorporate a parser to handle Gutenberg blocks more efficiently for improved performance [#22886] +* [**] Improve performance of Image and Gallery block processors to avoid long delay when saving a post [#22896] +* [*] Improve performance of File block processor [#22897] 24.9 ----- diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index 2ea04cc4724e..e0ef32d7d078 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -721,11 +721,8 @@ class PostCoordinator: NSObject { } // Ensure that all synced media references are up to date - post.media.forEach { media in - if media.remoteStatus == .sync { - self.updateReferences(to: media, in: post) - } - } + let syncedMedia = post.media.filter { $0.remoteStatus == .sync } + updateMediaBlocksBeforeSave(in: post, with: syncedMedia) let uuid = observeMedia(for: post, completion: completion) trackObserver(receipt: uuid, for: post) @@ -733,14 +730,21 @@ class PostCoordinator: NSObject { return } else { // Ensure that all media references are up to date - post.media.forEach { media in - self.updateReferences(to: media, in: post) - } + updateMediaBlocksBeforeSave(in: post, with: post.media) } completion(.success(post)) } + func updateMediaBlocksBeforeSave(in post: AbstractPost, with media: Set) { + guard let postContent = post.content else { + return + } + let contentParser = GutenbergContentParser(for: postContent) + media.forEach { self.updateReferences(to: $0, in: contentParser.blocks, post: post) } + post.content = contentParser.html() + } + // - warning: deprecated (kahu-offline-mode) func cancelAnyPendingSaveOf(post: AbstractPost) { removeObserver(for: post) @@ -879,7 +883,7 @@ class PostCoordinator: NSObject { switch state { case .ended: let successHandler = { - self.updateReferences(to: media, in: post) + self.updateMediaBlocksBeforeSave(in: post, with: [media]) if self.isSyncPublishingEnabled { if post.media.allSatisfy({ $0.remoteStatus == .sync }) { self.removeObserver(for: post) @@ -915,7 +919,7 @@ class PostCoordinator: NSObject { }, forMediaFor: post) } - private func updateReferences(to media: Media, in post: AbstractPost) { + private func updateReferences(to media: Media, in contentBlocks: [GutenbergParsedBlock], post: AbstractPost) { guard var postContent = post.content, let mediaID = media.mediaID?.intValue, let remoteURLStr = media.remoteURL else { @@ -935,19 +939,21 @@ class PostCoordinator: NSObject { if media.remoteStatus == .failed { return } - var gutenbergProcessors = [Processor]() - var aztecProcessors = [Processor]() + + var gutenbergBlockProcessors: [GutenbergProcessor] = [] + var gutenbergProcessors: [Processor] = [] + var aztecProcessors: [Processor] = [] // File block can upload any kind of media. let gutenbergFileProcessor = GutenbergFileUploadProcessor(mediaUploadID: gutenbergMediaUploadID, serverMediaID: mediaID, remoteURLString: remoteURLStr) - gutenbergProcessors.append(gutenbergFileProcessor) + gutenbergBlockProcessors.append(gutenbergFileProcessor) if media.mediaType == .image { let gutenbergImgPostUploadProcessor = GutenbergImgUploadProcessor(mediaUploadID: gutenbergMediaUploadID, serverMediaID: mediaID, remoteURLString: imageURL) - gutenbergProcessors.append(gutenbergImgPostUploadProcessor) + gutenbergBlockProcessors.append(gutenbergImgPostUploadProcessor) let gutenbergGalleryPostUploadProcessor = GutenbergGalleryUploadProcessor(mediaUploadID: gutenbergMediaUploadID, serverMediaID: mediaID, remoteURLString: imageURL, mediaLink: mediaLink) - gutenbergProcessors.append(gutenbergGalleryPostUploadProcessor) + gutenbergBlockProcessors.append(gutenbergGalleryPostUploadProcessor) let imgPostUploadProcessor = ImgUploadProcessor(mediaUploadID: mediaUploadID, remoteURLString: remoteURLStr, width: media.width?.intValue, height: media.height?.intValue) aztecProcessors.append(imgPostUploadProcessor) @@ -980,6 +986,7 @@ class PostCoordinator: NSObject { } // Gutenberg processors need to run first because they are more specific/and target only content inside specific blocks + gutenbergBlockProcessors.forEach { $0.process(contentBlocks) } postContent = gutenbergProcessors.reduce(postContent) { (content, processor) -> String in return processor.process(content) } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergFileUploadProcessor.swift b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergFileUploadProcessor.swift index d41cb2e7acec..7a16f8ac56aa 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergFileUploadProcessor.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergFileUploadProcessor.swift @@ -1,7 +1,6 @@ import Foundation -import Aztec -class GutenbergFileUploadProcessor: Processor { +class GutenbergFileUploadProcessor: GutenbergProcessor { private struct FileBlockKeys { static var name = "wp:file" static var id = "id" @@ -18,38 +17,24 @@ class GutenbergFileUploadProcessor: Processor { self.remoteURLString = remoteURLString } - lazy var fileHtmlProcessor = HTMLProcessor(for: "a", replacer: { (file) in - var attributes = file.attributes + func processFileBlocks(_ blocks: [GutenbergParsedBlock]) { + blocks.filter { $0.name == FileBlockKeys.name }.forEach { block in + guard let mediaID = block.attributes[FileBlockKeys.id] as? Int, + mediaID == self.mediaUploadID else { + return + } - attributes.set(.string(self.remoteURLString), forKey: FileBlockKeys.href) + // Update attributes + block.attributes[FileBlockKeys.id] = self.serverMediaID + block.attributes[FileBlockKeys.href] = self.remoteURLString - var html = "" - return html - }) - - lazy var fileBlockProcessor = GutenbergBlockProcessor(for: FileBlockKeys.name, replacer: { fileBlock in - guard let mediaID = fileBlock.attributes[FileBlockKeys.id] as? Int, - mediaID == self.mediaUploadID else { - return nil - } - var block = "" - block += self.fileHtmlProcessor.process(fileBlock.content) - block += "" - return block - }) + } - func process(_ text: String) -> String { - return fileBlockProcessor.process(text) + func process(_ blocks: [GutenbergParsedBlock]) { + processFileBlocks(blocks) } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift index bd1c2718f0b0..71afaf0cdadc 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift @@ -1,7 +1,7 @@ import Foundation -import Aztec +import SwiftSoup -class GutenbergGalleryUploadProcessor: Processor { +class GutenbergGalleryUploadProcessor: GutenbergProcessor { let mediaUploadID: Int32 let remoteURLString: String @@ -28,76 +28,70 @@ class GutenbergGalleryUploadProcessor: Processor { static let dataLink = "data-link" } - lazy var imgPostMediaUploadProcessor = HTMLProcessor(for: ImageKeys.name, replacer: { (img) in - guard let imgClassAttributeValue = img.attributes[ImageKeys.classAttributes]?.value, - case let .string(imgClass) = imgClassAttributeValue else { - return nil + func processImgPostMediaUpload(_ element: Element) { + guard let imgTags = try? element.select(ImageKeys.name) else { + return } + imgTags.forEach {imgTag in + guard let imgClass = try? imgTag.attr(ImageKeys.classAttributes) else { + return + } - let classAttributes = imgClass.components(separatedBy: " ") + let classAttributes = imgClass.components(separatedBy: " ") - guard let imageIDAttribute = classAttributes.filter({ (value) -> Bool in - value.hasPrefix(GutenbergImgUploadProcessor.imgClassIDPrefixAttribute) - }).first else { - return nil - } + guard let imageIDAttribute = classAttributes.filter({ (value) -> Bool in + value.hasPrefix(GutenbergImgUploadProcessor.imgClassIDPrefixAttribute) + }).first else { + return + } - let imageIDString = String(imageIDAttribute.dropFirst(ImageKeys.classIDPrefix.count)) - let imgUploadID = Int32(imageIDString) + let imageIDString = String(imageIDAttribute.dropFirst(ImageKeys.classIDPrefix.count)) + let imgUploadID = Int32(imageIDString) - guard imgUploadID == self.mediaUploadID else { - return nil - } + guard imgUploadID == self.mediaUploadID else { + return + } - let newImgClassAttributes = imgClass.replacingOccurrences(of: imageIDAttribute, with: ImageKeys.classIDPrefix + String(self.serverMediaID)) + let newImgClassAttributes = imgClass.replacingOccurrences(of: imageIDAttribute, with: ImageKeys.classIDPrefix + String(self.serverMediaID)) - var attributes = img.attributes - attributes.set(.string(self.remoteURLString), forKey: "src") - attributes.set(.string(newImgClassAttributes), forKey: "class") - attributes.set(.string("\(self.serverMediaID)"), forKey: ImageKeys.dataID) - attributes.set(.string(self.remoteURLString), forKey: ImageKeys.dataFullURL) - if attributes.contains(where: { $0.key == ImageKeys.dataLink } ) { - attributes.set(.string(self.mediaLink), forKey: ImageKeys.dataLink) - } + _ = try? imgTag.attr("src", self.remoteURLString) + _ = try? imgTag.attr("class", newImgClassAttributes) + _ = try? imgTag.attr(ImageKeys.dataID, String(self.serverMediaID)) + _ = try? imgTag.attr(ImageKeys.dataFullURL, self.remoteURLString) - var html = "<\(ImageKeys.name) " - let attributeSerializer = ShortcodeAttributeSerializer() - html += attributeSerializer.serialize(attributes) - html += " />" - return html - }) + if let _ = try? imgTag.attr(ImageKeys.dataLink) { + _ = try? imgTag.attr(ImageKeys.dataLink, self.mediaLink) + } + } + } private struct LinkKeys { static let name = "a" } - lazy var linkPostMediaUploadProcessor = HTMLProcessor(for: LinkKeys.name, replacer: { (link) in - - guard let linkOriginalContent = link.content else { - return nil + func processLinkPostMediaUpload(_ block: GutenbergParsedBlock) { + guard let aTags = try? block.elements.select(LinkKeys.name) else { + return } + aTags.forEach { aTag in + guard let linkOriginalContent = try? aTag.html() else { + return + } - let linkUpdatedContent = self.imgPostMediaUploadProcessor.process(linkOriginalContent) + processImgPostMediaUpload(aTag) + let linkUpdatedContent = try? aTag.html() - guard linkUpdatedContent != linkOriginalContent else { - return nil - } + guard linkUpdatedContent != linkOriginalContent else { + return + } - var attributes = link.attributes - if let linkToURL = self.linkToURL { - attributes.set(.string(linkToURL), forKey: "href") - } else { - attributes.set(.string(self.remoteURLString), forKey: "href") + if let linkToURL = self.linkToURL { + _ = try? aTag.attr("href", linkToURL) + } else { + _ = try? aTag.attr("href", self.remoteURLString) + } } - - var html = "<\(LinkKeys.name) " - let attributeSerializer = ShortcodeAttributeSerializer() - html += attributeSerializer.serialize(attributes) - html += " >" - html += linkUpdatedContent - html += "" - return html - }) + } private struct GalleryBlockKeys { static let name = "wp:gallery" @@ -117,42 +111,35 @@ class GutenbergGalleryUploadProcessor: Processor { return ids } - lazy var galleryBlockProcessor = GutenbergBlockProcessor(for: GalleryBlockKeys.name, replacer: { block in - guard let idsAny = block.attributes[GalleryBlockKeys.ids] as? [Any] else { - return nil - } - var ids = self.convertToIntArray(idsAny) - guard ids.contains(self.mediaUploadID) else { - return nil - } - var updatedBlock = "" - if let linkTo = block.attributes[GalleryBlockKeys.linkTo] as? String, linkTo != "none" { - if linkTo == "file" { - self.linkToURL = self.remoteURLString - } else if linkTo == "post" { - self.linkToURL = self.mediaLink + func processGalleryBlocks(_ blocks: [GutenbergParsedBlock]) { + let galleryBlocks = blocks.filter { $0.name == GalleryBlockKeys.name } + galleryBlocks.forEach { block in + guard let idsAny = block.attributes[GalleryBlockKeys.ids] as? [Any] else { + return + } + var ids = self.convertToIntArray(idsAny) + guard ids.contains(self.mediaUploadID) else { + return + } + if let index = ids.firstIndex(of: self.mediaUploadID ) { + ids[index] = Int32(self.serverMediaID) + } + block.attributes[GalleryBlockKeys.ids] = ids + + if let linkTo = block.attributes[GalleryBlockKeys.linkTo] as? String, linkTo != "none" { + if linkTo == "file" { + self.linkToURL = self.remoteURLString + } else if linkTo == "post" { + self.linkToURL = self.mediaLink + } + processLinkPostMediaUpload(block) + } else { + block.elements.forEach { processImgPostMediaUpload($0) } } - updatedBlock += self.linkPostMediaUploadProcessor.process(block.content) - } else { - updatedBlock += self.imgPostMediaUploadProcessor.process(block.content) } - updatedBlock += "" - return updatedBlock - }) + } - func process(_ text: String) -> String { - let result = galleryBlockProcessor.process(text) - return result + func process(_ blocks: [GutenbergParsedBlock]) { + processGalleryBlocks(blocks) } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift index eb8e7fd885c7..857a738f2962 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift @@ -1,7 +1,6 @@ import Foundation -import Aztec -class GutenbergImgUploadProcessor: Processor { +class GutenbergImgUploadProcessor: GutenbergProcessor { let mediaUploadID: Int32 let remoteURLString: String @@ -14,79 +13,58 @@ class GutenbergImgUploadProcessor: Processor { self.remoteURLString = remoteURLString } - lazy var imgPostMediaUploadProcessor = HTMLProcessor(for: "img", replacer: { (img) in - guard let imgClassAttributeValue = img.attributes["class"]?.value, - case let .string(imgClass) = imgClassAttributeValue else { - return nil - } - - let classAttributes = imgClass.components(separatedBy: " ") - - guard let imageIDAttribute = classAttributes.filter({ (value) -> Bool in - value.hasPrefix(GutenbergImgUploadProcessor.imgClassIDPrefixAttribute) - }).first else { - return nil - } - - let imageIDString = String(imageIDAttribute.dropFirst(GutenbergImgUploadProcessor.imgClassIDPrefixAttribute.count)) - let imgUploadID = Int32(imageIDString) + func processImgTags(_ block: GutenbergParsedBlock) { + let imgTags = try? block.elements.select("img") + imgTags?.forEach { img in + guard let imgClass = try? img.attr("class") else { + return + } + let classAttributes = imgClass.components(separatedBy: " ") - guard imgUploadID == self.mediaUploadID else { - return nil - } + guard let imageIDAttribute = classAttributes.filter({ (value) -> Bool in + value.hasPrefix(GutenbergImgUploadProcessor.imgClassIDPrefixAttribute) + }).first else { + return + } - let newImgClassAttributes = imgClass.replacingOccurrences(of: imageIDAttribute, with: GutenbergImgUploadProcessor.imgClassIDPrefixAttribute + String(self.serverMediaID)) + let imageIDString = String(imageIDAttribute.dropFirst(GutenbergImgUploadProcessor.imgClassIDPrefixAttribute.count)) + let imgUploadID = Int32(imageIDString) - var attributes = img.attributes - attributes.set(.string(self.remoteURLString), forKey: "src") - attributes.set(.string(newImgClassAttributes), forKey: "class") + guard imgUploadID == self.mediaUploadID else { + return + } - var html = " String { - var result = imgBlockProcessor.process(text) - result = mediaTextBlockProcessor.process(result) - return result + func process(_ blocks: [GutenbergParsedBlock]) { + processImageBlocks(blocks) + processMediaTextBlocks(blocks) } } diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 6e07ed45c207..d8e8080f5d03 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -880,6 +880,7 @@ 1D91080729F847A2003F9A5E /* MediaServiceUpdateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D91080629F847A2003F9A5E /* MediaServiceUpdateTests.m */; }; 1DE9F2B02BA30C930044AA53 /* GutenbergProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9F2AF2BA30C930044AA53 /* GutenbergProcessor.swift */; }; 1DE9F2B12BA30C930044AA53 /* GutenbergProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9F2AF2BA30C930044AA53 /* GutenbergProcessor.swift */; }; + 1DE9F2B32BA30E820044AA53 /* GutenbergFileUploadProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9F2B22BA30E820044AA53 /* GutenbergFileUploadProcessorTests.swift */; }; 1DF7A0CB2B9F66810003CBA3 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 1DF7A0CA2B9F66810003CBA3 /* SwiftSoup */; }; 1DF7A0CD2B9F66970003CBA3 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 1DF7A0CC2B9F66970003CBA3 /* SwiftSoup */; }; 1DF7A0CF2BA099760003CBA3 /* GutenbergContentParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF7A0CE2BA099760003CBA3 /* GutenbergContentParser.swift */; }; @@ -6634,6 +6635,7 @@ 1D6058910D05DD3D006BFB54 /* WordPress.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WordPress.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1D91080629F847A2003F9A5E /* MediaServiceUpdateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MediaServiceUpdateTests.m; sourceTree = ""; }; 1DE9F2AF2BA30C930044AA53 /* GutenbergProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergProcessor.swift; sourceTree = ""; }; + 1DE9F2B22BA30E820044AA53 /* GutenbergFileUploadProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergFileUploadProcessorTests.swift; sourceTree = ""; }; 1DF7A0CE2BA099760003CBA3 /* GutenbergContentParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergContentParser.swift; sourceTree = ""; }; 1DF7A0D22BA0B1810003CBA3 /* GutenbergContentParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergContentParser.swift; sourceTree = ""; }; 1E0462152566938300EB98EF /* GutenbergFileUploadProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergFileUploadProcessor.swift; sourceTree = ""; }; @@ -19010,6 +19012,7 @@ AEE082892681C23C00DCF54B /* GutenbergRefactoredGalleryUploadProcessorTests.swift */, FE9438B12A050251006C40EC /* BlockEditorSettings_GutenbergEditorSettingsTests.swift */, 1DF7A0D22BA0B1810003CBA3 /* GutenbergContentParser.swift */, + 1DE9F2B22BA30E820044AA53 /* GutenbergFileUploadProcessorTests.swift */, ); name = Gutenberg; sourceTree = ""; @@ -24055,6 +24058,7 @@ F41D98E12B39C5CE004EC050 /* BlogDashboardDynamicCardCoordinatorTests.swift in Sources */, FE2E3729281C839C00A1E82A /* BloggingPromptsServiceTests.swift in Sources */, D848CC0720FF2BE200A9038F /* NotificationContentRangeFactoryTests.swift in Sources */, + 1DE9F2B32BA30E820044AA53 /* GutenbergFileUploadProcessorTests.swift in Sources */, 732A473F21878EB10015DA74 /* WPRichContentViewTests.swift in Sources */, 8BDA5A6D247C2F8400AB124C /* ReaderDetailViewControllerTests.swift in Sources */, 82301B8F1E787420009C9C4E /* AppRatingUtilityTests.swift in Sources */, diff --git a/WordPress/WordPressTest/Gutenberg/GutenbergGalleryUploadProcessorTests.swift b/WordPress/WordPressTest/Gutenberg/GutenbergGalleryUploadProcessorTests.swift index 7c6da6c29a8d..b320e9f9d03c 100644 --- a/WordPress/WordPressTest/Gutenberg/GutenbergGalleryUploadProcessorTests.swift +++ b/WordPress/WordPressTest/Gutenberg/GutenbergGalleryUploadProcessorTests.swift @@ -60,7 +60,7 @@ class GutenbergGalleryUploadProcessorTests: XCTestCase {