From e1990507beb2850960355c803a6d5276301e4aa8 Mon Sep 17 00:00:00 2001 From: Nolan Waite Date: Sat, 6 Jan 2024 03:09:10 -0400 Subject: [PATCH 1/2] Move remaining WKWebView evaluateJavaScript calls to async-await and make a non-crashing async-await version --- App/Extensions/WKWebView+eval.swift | 25 ++++ .../AcknowledgementsViewController.swift | 20 ++-- .../Posts/PostsPageViewController.swift | 4 - App/Views/RenderView.swift | 111 +++++++++++------- Awful.xcodeproj/project.pbxproj | 4 + 5 files changed, 110 insertions(+), 54 deletions(-) create mode 100644 App/Extensions/WKWebView+eval.swift diff --git a/App/Extensions/WKWebView+eval.swift b/App/Extensions/WKWebView+eval.swift new file mode 100644 index 000000000..097a21790 --- /dev/null +++ b/App/Extensions/WKWebView+eval.swift @@ -0,0 +1,25 @@ +// WKWebView+eval.swift +// +// Copyright 2015 Awful Contributors. CC BY-NC-SA 3.0 US https://github.com/Awful/Awful.app + +import WebKit + +extension WKWebView { + /** + Evaluates the specified JavaScript string. + + The async-await version of `evaluateJavaScript(_:)` in the overlay assumes a non-`nil` result of evaluating `javaScript`. This async-await version does not make that assumption. + */ + @discardableResult + func eval(_ javaScript: String) async throws -> Any? { + try await withCheckedThrowingContinuation { continuation in + evaluateJavaScript(javaScript, completionHandler: { result, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: result) + } + }) + } + } +} diff --git a/App/View Controllers/AcknowledgementsViewController.swift b/App/View Controllers/AcknowledgementsViewController.swift index 4851c838a..649eb6ffb 100644 --- a/App/View Controllers/AcknowledgementsViewController.swift +++ b/App/View Controllers/AcknowledgementsViewController.swift @@ -64,17 +64,19 @@ final class AcknowledgementsViewController: ViewController { override func themeDidChange() { super.themeDidChange() - - let js = """ - var s = document.body.style; - s.backgroundColor = "\(backgroundColor.hexCode)"; - s.color = "\(textColor.hexCode)"; - """ - webView.evaluateJavaScript(js, completionHandler: { result, error in - if let error = error { + + Task { + let js = """ + var s = document.body.style; + s.backgroundColor = "\(backgroundColor.hexCode)"; + s.color = "\(textColor.hexCode)"; + """ + do { + try await webView.eval(js) + } catch { Log.e("error running script `\(js)` in acknowledgements screen: \(error)") } - }) + } } override func viewWillAppear(_ animated: Bool) { diff --git a/App/View Controllers/Posts/PostsPageViewController.swift b/App/View Controllers/Posts/PostsPageViewController.swift index 694da8fab..775f0be04 100644 --- a/App/View Controllers/Posts/PostsPageViewController.swift +++ b/App/View Controllers/Posts/PostsPageViewController.swift @@ -1026,8 +1026,6 @@ final class PostsPageViewController: ViewController { self.navigationController?.pushViewController(postsVC, animated: true) - print("Your Posts") - } } @@ -1294,8 +1292,6 @@ final class PostsPageViewController: ViewController { possessiveUsername = "\(self.selectedPost!.author?.username ?? "")'s" } - print("\(possessiveUsername)") - let postActionMenu: UIMenu = { // edit post if self.selectedPost!.editable { diff --git a/App/Views/RenderView.swift b/App/Views/RenderView.swift index fc83f1d29..32fdd952c 100644 --- a/App/Views/RenderView.swift +++ b/App/Views/RenderView.swift @@ -90,15 +90,17 @@ final class RenderView: UIView { - Seealso: `UIScrollView.fractionalContentOffset`. */ func scrollToFractionalOffset(_ fractionalOffset: CGPoint) { - webView.evaluateJavaScript(""" - window.scrollTo( - document.body.scrollWidth * \(fractionalOffset.x), - document.body.scrollHeight * \(fractionalOffset.y)); - """, completionHandler: { result, error in - if let error = error { + Task { + do { + try await webView.eval(""" + window.scrollTo( + document.body.scrollWidth * \(fractionalOffset.x), + document.body.scrollHeight * \(fractionalOffset.y)); + """) + } catch { Log.e("error attempting to scroll: \(error)") } - }) + } } // MARK: Gunk @@ -274,22 +276,25 @@ extension RenderView { /// Turns any links that look like tweets into an actual tweet embed. func embedTweets() { let renderGhostTweets = UserDefaults.standard.enableFrogAndGhost - webView.evaluateJavaScript("if (window.Awful) { Awful.renderGhostTweets = \(renderGhostTweets); Awful.embedTweets(); }") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) { Awful.renderGhostTweets = \(renderGhostTweets); Awful.embedTweets(); }") + } catch { self.mentionError(error, explanation: "could not evaluate embedTweets") } } } func loadLottiePlayer() { - webView.evaluateJavaScript("if (window.Awful) Awful.loadLotties()") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.loadLotties()") + } catch { self.mentionError(error, explanation: "could not evaluate loadLotties") } } } - - + /// iOS 15 and transparent webviews = dark "missing" scroll thumbs, regardless of settings applied /// webview must be transparent to prevent white flashes during content refreshes. setting opaque to true in viewDidAppear helped, but still sometimes produced white flashes. /// instead, we toggle the webview to opaque while it's being scrolled and return it to transparent seconds after @@ -317,7 +322,7 @@ extension RenderView { do { // There's a bit of subtlety here: `document.open()` returns a Document, which can't be serialized back to the native-side of the app; and if we don't include a ``, we get console logs attempting to e.g. retrieve `document.body.scrollWidth`. - try await webView.evaluateJavaScript("document.open(), document.write('')") + try await webView.eval("document.open(), document.write('')") Log.d("did erase document") } catch { mentionError(error, explanation: "could not remove content") @@ -371,7 +376,7 @@ extension RenderView { let rawResult: Any? do { - rawResult = try await webView.evaluateJavaScript("if (window.Awful) Awful.interestingElementsAtPoint(\(point.x), \(point.y))") + rawResult = try await webView.eval("if (window.Awful) Awful.interestingElementsAtPoint(\(point.x), \(point.y))") } catch { mentionError(error, explanation: "could not evaluate interestingElementsAtPoint") return [] @@ -470,8 +475,10 @@ extension RenderView { Log.w("could not JSON-escape the post ID: \(error)") return } - webView.evaluateJavaScript("if (window.Awful) Awful.jumpToPostWithID(\(escapedPostID))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.jumpToPostWithID(\(escapedPostID))") + } catch { self.mentionError(error, explanation: "could not evaluate jumpToPostWithID") } } @@ -479,8 +486,10 @@ extension RenderView { /// Turns each link with a `data-awful-linkified-image` attribute into a a proper `img` element. func loadLinkifiedImages() { - webView.evaluateJavaScript("if (window.Awful) Awful.loadLinkifiedImages()") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.loadLinkifiedImages()") + } catch { self.mentionError(error, explanation: "could not evaluate loadLinkifiedImages") } } @@ -492,7 +501,7 @@ extension RenderView { let js = "if (window.Awful) Awful.postElementAtPoint(\(point.x), \(point.y))" let result: Any? do { - result = try await webView.evaluateJavaScript(js) + result = try await webView.eval(js) } catch { mentionError(error, explanation: "could not evaluate findPostFrame") return nil @@ -511,8 +520,10 @@ extension RenderView { return } - webView.evaluateJavaScript("if (window.Awful) Awful.markReadUpToPostWithID(\(escaped))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.markReadUpToPostWithID(\(escaped))") + } catch { self.mentionError(error, explanation: "could not evaluate markReadUpToPostWithID") } } @@ -528,8 +539,10 @@ extension RenderView { return } - webView.evaluateJavaScript("if (window.Awful) Awful.prependPosts(\(escaped))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.prependPosts(\(escaped))") + } catch { self.mentionError(error, explanation: "could not evaluate prependPosts") } } @@ -545,8 +558,10 @@ extension RenderView { return } - webView.evaluateJavaScript("if (window.Awful) Awful.setPostHTMLAtIndex(\(escaped), \(i))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.setPostHTMLAtIndex(\(escaped), \(i))") + } catch { self.mentionError(error, explanation: "could not evaluate setPostHTMLAtIndex") } } @@ -562,8 +577,10 @@ extension RenderView { return } - webView.evaluateJavaScript("if (window.Awful) Awful.setExternalStylesheet(\(escaped))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.setExternalStylesheet(\(escaped))") + } catch { self.mentionError(error, explanation: "could not evaluate setExternalStylesheet") } } @@ -571,8 +588,10 @@ extension RenderView { /// Sets the font scale to the specified number of percentage points. e.g. for `font-scale: 50%` you would pass in `50`. func setFontScale(_ scale: Double) { - webView.evaluateJavaScript("if (window.Awful) Awful.setFontScale(\(scale))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.setFontScale(\(scale))") + } catch { self.mentionError(error, explanation: "could not evaluate setFontScale") } } @@ -592,8 +611,10 @@ extension RenderView { return } - webView.evaluateJavaScript("if (window.Awful) Awful.fyadFlag.setFlag(\(escaped))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.fyadFlag.setFlag(\(escaped))") + } catch { self.mentionError(error, explanation: "could not evaluate setFYADFlag") } } @@ -606,8 +627,10 @@ extension RenderView { /// Toggles the `highlight` class in all username mentions in post bodies, adding it when `true` or removing it when `false`. func setHighlightMentions(_ highlightMentions: Bool) { - webView.evaluateJavaScript("if (window.Awful) Awful.setHighlightMentions(\(highlightMentions ? "true" : "false"))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.setHighlightMentions(\(highlightMentions ? "true" : "false"))") + } catch { self.mentionError(error, explanation: "could not evaluate setHighlightMentions") } } @@ -615,8 +638,10 @@ extension RenderView { /// Turns all avatars on (when `true`) or off (when `false`). func setShowAvatars(_ showAvatars: Bool) { - webView.evaluateJavaScript("if (window.Awful) Awful.setShowAvatars(\(showAvatars ? "true" : "false"))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.setShowAvatars(\(showAvatars ? "true" : "false"))") + } catch { self.mentionError(error, explanation: "could not evaluate setShowAvatars") } } @@ -632,8 +657,10 @@ extension RenderView { return } - webView.evaluateJavaScript("if (window.Awful) Awful.setThemeStylesheet(\(escaped))") { rawResult, error in - if let error = error { + Task { + do { + _ = try await webView.eval("if (window.Awful) Awful.setThemeStylesheet(\(escaped))") + } catch { self.mentionError(error, explanation: "could not evaluate setThemeStylesheet") } } @@ -648,8 +675,10 @@ extension RenderView { return } - webView.evaluateJavaScript("if (window.Awful) Awful.setTweetTheme(\(escaped))") { rawResult, error in - if let error = error { + Task { + do { + try await webView.eval("if (window.Awful) Awful.setTweetTheme(\(escaped))") + } catch { self.mentionError(error, explanation: "could not evaluate Awful.setTweetTheme") } } @@ -675,7 +704,7 @@ extension RenderView { Awful.unionFrameOfElements( document.querySelectorAll(\(escapedSelector))); """ - rawResult = try await webView.evaluateJavaScript(js) + rawResult = try await webView.eval(js) } catch { mentionError(error, explanation: "could not evaluate unionFrameOfElements") return .null diff --git a/Awful.xcodeproj/project.pbxproj b/Awful.xcodeproj/project.pbxproj index 55f1bff81..743dbf1d6 100644 --- a/Awful.xcodeproj/project.pbxproj +++ b/Awful.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ 1C4E68311B32558A00FC2A02 /* ForumListSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4E68301B32558A00FC2A02 /* ForumListSectionHeaderView.swift */; }; 1C4EAD5E1BC0622D0008BE54 /* AwfulCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4EAD5D1BC0622D0008BE54 /* AwfulCore.swift */; }; 1C4EE1CE28470C2400A7507E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2D0BB35827F440AF00242D2A /* Assets.xcassets */; }; + 1C4F0A312B49311A00467112 /* WKWebView+eval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4F0A302B49311A00467112 /* WKWebView+eval.swift */; }; 1C54D30B19BBC717002AC6B9 /* SettingsBinding.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C54D30A19BBC717002AC6B9 /* SettingsBinding.m */; }; 1C54D31119BE1441002AC6B9 /* SettingsAvatarHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C54D31019BE1441002AC6B9 /* SettingsAvatarHeader.swift */; }; 1C54D31319BE1708002AC6B9 /* SettingsAvatarHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1C54D31219BE1708002AC6B9 /* SettingsAvatarHeader.xib */; }; @@ -505,6 +506,7 @@ 1C48538D1F93BADF0042531A /* HTMLRenderingHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLRenderingHelpers.swift; sourceTree = ""; }; 1C4E68301B32558A00FC2A02 /* ForumListSectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForumListSectionHeaderView.swift; sourceTree = ""; }; 1C4EAD5D1BC0622D0008BE54 /* AwfulCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AwfulCore.swift; sourceTree = ""; }; + 1C4F0A302B49311A00467112 /* WKWebView+eval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKWebView+eval.swift"; sourceTree = ""; }; 1C54D30919BBC717002AC6B9 /* SettingsBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsBinding.h; sourceTree = ""; }; 1C54D30A19BBC717002AC6B9 /* SettingsBinding.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsBinding.m; sourceTree = ""; }; 1C54D31019BE1441002AC6B9 /* SettingsAvatarHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAvatarHeader.swift; sourceTree = ""; }; @@ -1196,6 +1198,7 @@ 1C453EDD2336B694007AC6CD /* UITextView+Selections.swift */, 1CD7EED7228504C100B62865 /* UIView+CompressedHeight.swift */, 1C9623A02B002ECB000B232B /* UIViewController+async.swift */, + 1C4F0A302B49311A00467112 /* WKWebView+eval.swift */, ); path = Extensions; sourceTree = ""; @@ -1919,6 +1922,7 @@ 1C40796A1A228DA6004A082F /* CopyURLActivity.swift in Sources */, 83410EF219A582B8002CD019 /* DateFormatters.swift in Sources */, 1C273A9E21B316DB002875A9 /* LoadMoreFooter.swift in Sources */, + 1C4F0A312B49311A00467112 /* WKWebView+eval.swift in Sources */, 1C2C1F0E1CE16FE200CD27DD /* CloseBBcodeTagCommand.swift in Sources */, 1C16FBF31CBDC58B00C88BD1 /* URL+OpensInBrowser.swift in Sources */, 1C4E68311B32558A00FC2A02 /* ForumListSectionHeaderView.swift in Sources */, From 7950c4a8a401b2dbe1b77039e62f3cb8e33523f7 Mon Sep 17 00:00:00 2001 From: Nolan Waite Date: Sat, 6 Jan 2024 03:30:17 -0400 Subject: [PATCH 2/2] Remove iOS 13- fallback menu from posts page --- .../Posts/PostsPageViewController.swift | 82 +++++++------------ 1 file changed, 31 insertions(+), 51 deletions(-) diff --git a/App/View Controllers/Posts/PostsPageViewController.swift b/App/View Controllers/Posts/PostsPageViewController.swift index 775f0be04..6c02a8319 100644 --- a/App/View Controllers/Posts/PostsPageViewController.swift +++ b/App/View Controllers/Posts/PostsPageViewController.swift @@ -33,56 +33,43 @@ final class PostsPageViewController: ViewController { private var webViewDidLoadOnce = false lazy var threadActionsMenu: UIMenu = { - var threadActions: [UIMenuElement] = [] - - let bookmarkTitle = self.thread.bookmarked ? "Remove Bookmark" : "Bookmark Thread" - let bookmarkImage = self.thread.bookmarked ? - UIImage(named: "remove-bookmark")!.withRenderingMode(.alwaysTemplate) - : - UIImage(named: "add-bookmark")!.withRenderingMode(.alwaysTemplate) - let yourPostsImage = UIImage(named: "single-users-posts")!.withRenderingMode(.alwaysTemplate) - let copyURLImage = UIImage(named: "copy-url")!.withRenderingMode(.alwaysTemplate) - let voteImage = UIImage(named: "vote")!.withRenderingMode(.alwaysTemplate) - + var threadActions: [UIAction] = [] + // Bookmark - let bookmarkIdentifier = UIAction.Identifier("bookmark") - let bookmarkAction = UIAction(title: bookmarkTitle, image: bookmarkImage, identifier: bookmarkIdentifier, handler: { [unowned self] action in - bookmark(action: action) - }) - bookmarkAction.attributes = self.thread.bookmarked ? [.destructive] : [] + let bookmarkAction = UIAction( + title: thread.bookmarked ? "Remove Bookmark" : "Bookmark Thread", + image: UIImage(named: thread.bookmarked ? "remove-bookmark" : "add-bookmark")!.withRenderingMode(.alwaysTemplate), + identifier: .init("bookmark"), + handler: { [unowned self] in bookmark(action: $0) } + ) + bookmarkAction.attributes = thread.bookmarked ? .destructive : [] threadActions.append(bookmarkAction) // Copy link - let copyLinkIdentifier = UIAction.Identifier("copyLink") - let copyLinkAction = UIAction(title: "Copy link", image: copyURLImage, identifier: copyLinkIdentifier, handler: { [unowned self] action in - copyLink(action: action) - }) - threadActions.append(copyLinkAction) - + threadActions.append(.init( + title: "Copy link", + image: UIImage(named: "copy-url")!.withRenderingMode(.alwaysTemplate), + identifier: .init("copyLink"), + handler: { [unowned self] in copyLink(action: $0) } + )) + // Vote - let voteIdentifier = UIAction.Identifier("vote") - let voteAction = UIAction(title: "Vote", image: voteImage, identifier: voteIdentifier, handler: { [unowned self] action in - vote(action: action) - }) - threadActions.append(voteAction) - + threadActions.append(.init( + title: "Vote", + image: UIImage(named: "vote")!.withRenderingMode(.alwaysTemplate), + identifier: .init("vote"), + handler: { [unowned self] in vote(action: $0) } + )) + // Your posts - let yourPostsIdentifier = UIAction.Identifier("yourPosts") - let yourPostsAction = UIAction(title: "Your posts", image: yourPostsImage, identifier: yourPostsIdentifier, handler: { [unowned self] action in - yourPosts(action: action) - }) - threadActions.append(yourPostsAction) - - if #available(iOS 14.0, *) { - // no op. iOS14+ uses UIMenu, 13 uses Chidori third party menus - } else { - actionMappings[bookmarkIdentifier] = bookmark(action:) - actionMappings[copyLinkIdentifier] = copyLink(action:) - actionMappings[voteIdentifier] = vote(action:) - actionMappings[yourPostsIdentifier] = yourPosts(action:) - } - - return UIMenu(title: self.thread.title ?? "", image: nil, identifier: nil, options: [.displayInline], children: threadActions) + threadActions.append(.init( + title: "Your posts", + image: UIImage(named: "single-users-posts")!.withRenderingMode(.alwaysTemplate), + identifier: .init("yourPosts"), + handler: { [unowned self] in yourPosts(action: $0) } + )) + + return UIMenu(title: thread.title ?? "", image: nil, identifier: nil, options: .displayInline, children: threadActions) }() @@ -1285,13 +1272,6 @@ final class PostsPageViewController: ViewController { self.selectedPost = posts[postIndex + hiddenPosts] self.selectedFrame = frame - let possessiveUsername: String - if self.selectedPost!.author?.username == UserDefaults.standard.loggedInUsername { - possessiveUsername = "Your" - } else { - possessiveUsername = "\(self.selectedPost!.author?.username ?? "")'s" - } - let postActionMenu: UIMenu = { // edit post if self.selectedPost!.editable {