From a35b72d256d59eede6e6e115d6953418ed633c13 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 26 Dec 2024 17:31:18 -0500 Subject: [PATCH] Revert "Replace WebURL with URL.ParseStrategy" This reverts commit adaf8dc217f0857bef9b8dcc7fae0efc3c08bc3d. --- .../NotificationService.swift | 16 +++- Packages/Pachyderm/Package.swift | 4 +- .../Pachyderm/Sources/Pachyderm/Client.swift | 5 +- .../Pachyderm/Model/Announcement.swift | 5 +- .../Sources/Pachyderm/Model/Card.swift | 25 +++--- .../Sources/Pachyderm/Model/Emoji.swift | 16 +++- .../Sources/Pachyderm/Model/Hashtag.swift | 8 +- .../Sources/Pachyderm/Model/Mention.swift | 12 ++- .../Pachyderm/Model/Notification.swift | 5 +- .../Pachyderm/Model/PushNotification.swift | 5 +- .../Sources/Pachyderm/Model/Status.swift | 10 +-- .../Utilities/NotificationGroup.swift | 3 +- .../Pachyderm/Utilities/URLDecoder.swift | 86 ------------------- .../Tests/PachydermTests/URLTests.swift | 21 +++-- ShareExtension/ShareHostingController.swift | 3 +- Tusker.xcodeproj/project.pbxproj | 25 ++++++ Tusker/CoreData/FollowedHashtag.swift | 3 +- Tusker/CoreData/SavedHashtag.swift | 3 +- Tusker/CoreData/StatusMO.swift | 3 +- Tusker/HTMLConverter.swift | 15 +++- .../AnnouncementContentTextView.swift | 5 +- .../Announcements/AnnouncementListRow.swift | 5 +- .../ConversationViewController.swift | 6 +- .../Explore/ExploreViewController.swift | 6 +- .../TrendingHashtagsViewController.swift | 6 +- .../TrendingLinkCardCollectionViewCell.swift | 3 +- .../Explore/TrendingLinkCardView.swift | 1 + .../Explore/TrendingLinksViewController.swift | 14 +-- .../Explore/TrendsViewController.swift | 19 +++- .../FastSwitchingAccountView.swift | 7 +- ...nNotificationGroupCollectionViewCell.swift | 2 +- ...otificationsCollectionViewController.swift | 2 +- .../Appearance/MockStatusView.swift | 5 +- .../Preferences/PrefsAccountView.swift | 8 +- .../Search/SearchResultsViewController.swift | 3 +- ...tatusActionAccountListViewController.swift | 3 +- Tusker/Screens/Utilities/Previewing.swift | 17 +++- Tusker/Views/AccountDisplayNameView.swift | 3 +- Tusker/Views/BaseEmojiLabel.swift | 7 +- Tusker/Views/ContentTextView.swift | 2 + Tusker/Views/CustomEmojiImageView.swift | 3 +- Tusker/Views/Status/StatusCardView.swift | 24 +++--- Tusker/Views/StatusContentTextView.swift | 5 +- 43 files changed, 238 insertions(+), 191 deletions(-) delete mode 100644 Packages/Pachyderm/Sources/Pachyderm/Utilities/URLDecoder.swift diff --git a/NotificationExtension/NotificationService.swift b/NotificationExtension/NotificationService.swift index 9e00a67d..a2754ee4 100644 --- a/NotificationExtension/NotificationService.swift +++ b/NotificationExtension/NotificationService.swift @@ -14,6 +14,7 @@ import OSLog import Pachyderm import Intents import HTMLStreamer +import WebURL import UIKit import TuskerPreferences @@ -237,7 +238,8 @@ class NotificationService: UNNotificationServiceExtension { for match in emojiRegex.matches(in: content.body, range: NSRange(location: 0, length: content.body.utf16.count)).reversed() { let emojiName = (content.body as NSString).substring(with: match.range(at: 1)) guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }), - let (data, _) = try? await URLSession.shared.data(from: emoji.url), + let url = URL(emoji.url), + let (data, _) = try? await URLSession.shared.data(from: url), let image = UIImage(data: data) else { continue } @@ -366,7 +368,17 @@ private func decodeBase64URL(_ s: String) -> Data? { // copied from HTMLConverter.Callbacks, blergh private struct HTMLCallbacks: HTMLConversionCallbacks { static func makeURL(string: String) -> URL? { - try? URL.ParseStrategy().parse(string) + // Converting WebURL to URL is a small but non-trivial expense (since it works by + // serializing the WebURL as a string and then having Foundation parse it again), + // so, if available, use the system parser which doesn't require another round trip. + if let url = try? URL.ParseStrategy().parse(string) { + url + } else if let web = WebURL(string), + let url = URL(web) { + url + } else { + nil + } } static func elementAction(name: String, attributes: [Attribute]) -> ElementAction { diff --git a/Packages/Pachyderm/Package.swift b/Packages/Pachyderm/Package.swift index 959bd7b1..a1be7f1d 100644 --- a/Packages/Pachyderm/Package.swift +++ b/Packages/Pachyderm/Package.swift @@ -7,7 +7,6 @@ let package = Package( name: "Pachyderm", platforms: [ .iOS(.v16), - .macOS(.v13), ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. @@ -17,6 +16,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/karwa/swift-url.git", exact: "0.4.2"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -24,6 +24,8 @@ let package = Package( .target( name: "Pachyderm", dependencies: [ + .product(name: "WebURL", package: "swift-url"), + .product(name: "WebURLFoundationExtras", package: "swift-url"), ], swiftSettings: [ .swiftLanguageMode(.v5) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Client.swift b/Packages/Pachyderm/Sources/Pachyderm/Client.swift index f5f157e5..6065e841 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Client.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Client.swift @@ -7,6 +7,7 @@ // import Foundation +import WebURL /** The base Mastodon API client. @@ -201,8 +202,8 @@ public struct Client: Sendable { let wellKnown = Request(method: .get, path: "/.well-known/nodeinfo") let wellKnownResults = try await run(wellKnown).0 if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }), - let href = try? URL.ParseStrategy().parse(url.href), - href.host == self.baseURL.host() { + let href = WebURL(url.href), + href.host == WebURL(self.baseURL)?.host { let nodeInfo = Request(method: .get, path: Endpoint(stringLiteral: href.path)) return try await run(nodeInfo).0 } else { diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Announcement.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Announcement.swift index 1d7c61f1..b4735994 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Announcement.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Announcement.swift @@ -6,6 +6,7 @@ // import Foundation +import WebURL public struct Announcement: Decodable, Sendable, Hashable, Identifiable { public let id: String @@ -59,7 +60,7 @@ extension Announcement { public struct Account: Decodable, Sendable, Hashable { public let id: String public let username: String - @URLDecoder public var url: URL + public let url: WebURL public let acct: String } } @@ -67,7 +68,7 @@ extension Announcement { extension Announcement { public struct Status: Decodable, Sendable, Hashable { public let id: String - @URLDecoder public var url: URL + public let url: WebURL } } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift index f60756a2..f3dbd14e 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Card.swift @@ -7,17 +7,18 @@ // import Foundation +import WebURL public struct Card: Codable, Sendable { - @URLDecoder public var url: URL + public let url: WebURL public let title: String public let description: String - @OptionalURLDecoder public var image: URL? + public let image: WebURL? public let kind: Kind public let authorName: String? - @OptionalURLDecoder public var authorURL: URL? + public let authorURL: WebURL? public let providerName: String? - @OptionalURLDecoder public var providerURL: URL? + public let providerURL: WebURL? public let html: String? public let width: Int? public let height: Int? @@ -26,15 +27,15 @@ public struct Card: Codable, Sendable { public let history: [History]? public init( - url: URL, + url: WebURL, title: String, description: String, - image: URL? = nil, + image: WebURL? = nil, kind: Card.Kind, authorName: String? = nil, - authorURL: URL? = nil, + authorURL: WebURL? = nil, providerName: String? = nil, - providerURL: URL? = nil, + providerURL: WebURL? = nil, html: String? = nil, width: Int? = nil, height: Int? = nil, @@ -60,15 +61,15 @@ public struct Card: Codable, Sendable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self._url = try container.decode(URLDecoder.self, forKey: .url) + self.url = try container.decode(WebURL.self, forKey: .url) self.title = try container.decode(String.self, forKey: .title) self.description = try container.decode(String.self, forKey: .description) self.kind = try container.decode(Kind.self, forKey: .kind) - self._image = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .image) ?? nil + self.image = try? container.decodeIfPresent(WebURL.self, forKey: .image) self.authorName = try? container.decodeIfPresent(String.self, forKey: .authorName) - self._authorURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .authorURL) ?? nil + self.authorURL = try? container.decodeIfPresent(WebURL.self, forKey: .authorURL) self.providerName = try? container.decodeIfPresent(String.self, forKey: .providerName) - self._providerURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .providerURL) ?? nil + self.providerURL = try? container.decodeIfPresent(WebURL.self, forKey: .providerURL) self.html = try? container.decodeIfPresent(String.self, forKey: .html) self.width = try? container.decodeIfPresent(Int.self, forKey: .width) self.height = try? container.decodeIfPresent(Int.self, forKey: .height) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Emoji.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Emoji.swift index 1d08e99c..8a11f139 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Emoji.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Emoji.swift @@ -7,11 +7,14 @@ // import Foundation +import WebURL public struct Emoji: Codable, Sendable { public let shortcode: String - @URLDecoder public var url: URL - @URLDecoder public var staticURL: URL + // these shouldn't need to be WebURLs as they're not external resources, + // but some instances (pleroma?) has emoji urls that Foundation considers malformed so we use WebURL to be more lenient + public let url: WebURL + public let staticURL: WebURL public let visibleInPicker: Bool public let category: String? @@ -19,8 +22,13 @@ public struct Emoji: Codable, Sendable { let container = try decoder.container(keyedBy: CodingKeys.self) self.shortcode = try container.decode(String.self, forKey: .shortcode) - self._url = try container.decode(URLDecoder.self, forKey: .url) - self._staticURL = try container.decode(URLDecoder.self, forKey: .staticURL) + do { + self.url = try container.decode(WebURL.self, forKey: .url) + } catch { + let s = try? container.decode(String.self, forKey: .url) + throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath + [CodingKeys.url], debugDescription: "Could not decode URL '\(s ?? "")'", underlyingError: error)) + } + self.staticURL = try container.decode(WebURL.self, forKey: .staticURL) self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker) self.category = try container.decodeIfPresent(String.self, forKey: .category) } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Hashtag.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Hashtag.swift index 3d86209a..85b2c877 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Hashtag.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Hashtag.swift @@ -7,10 +7,12 @@ // import Foundation +import WebURL +import WebURLFoundationExtras public struct Hashtag: Codable, Sendable { public let name: String - @URLDecoder public var url: URL + public let url: WebURL /// Only present when returned from the trending hashtags endpoint public let history: [History]? /// Only present on Mastodon >= 4 and when logged in @@ -18,7 +20,7 @@ public struct Hashtag: Codable, Sendable { public init(name: String, url: URL) { self.name = name - self.url = url + self.url = WebURL(url)! self.history = nil self.following = nil } @@ -27,7 +29,7 @@ public struct Hashtag: Codable, Sendable { let container = try decoder.container(keyedBy: CodingKeys.self) self.name = try container.decode(String.self, forKey: .name) // pixelfed (possibly others) don't fully escape special characters in the hashtag url - self._url = try container.decode(URLDecoder.self, forKey: .url) + self.url = try container.decode(WebURL.self, forKey: .url) self.history = try container.decodeIfPresent([History].self, forKey: .history) self.following = try container.decodeIfPresent(Bool.self, forKey: .following) } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Mention.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Mention.swift index d48e13c7..f92d3c46 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Mention.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Mention.swift @@ -7,9 +7,10 @@ // import Foundation +import WebURL public struct Mention: Codable, Sendable { - @URLDecoder public var url: URL + public let url: WebURL public let username: String public let acct: String /// The instance-local ID of the user being mentioned. @@ -20,10 +21,15 @@ public struct Mention: Codable, Sendable { self.username = try container.decode(String.self, forKey: .username) self.acct = try container.decode(String.self, forKey: .acct) self.id = try container.decode(String.self, forKey: .id) - self._url = try container.decode(URLDecoder.self, forKey: .url) + do { + self.url = try container.decode(WebURL.self, forKey: .url) + } catch { + let s = try? container.decode(String.self, forKey: .url) + throw DecodingError.dataCorruptedError(forKey: .url, in: container, debugDescription: "Could not decode URL '\(s ?? "")'") + } } - public init(url: URL, username: String, acct: String, id: String) { + public init(url: WebURL, username: String, acct: String, id: String) { self.url = url self.username = username self.acct = acct diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Notification.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Notification.swift index 303e37bb..2053b4a4 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Notification.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Notification.swift @@ -7,6 +7,7 @@ // import Foundation +import WebURL public struct Notification: Decodable, Sendable { public let id: String @@ -17,7 +18,7 @@ public struct Notification: Decodable, Sendable { // Only present for pleroma emoji reactions // Either an emoji or :shortcode: (for akkoma custom emoji reactions) public let emoji: String? - @OptionalURLDecoder public var emojiURL: URL? + public let emojiURL: WebURL? public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -32,7 +33,7 @@ public struct Notification: Decodable, Sendable { self.account = try container.decode(Account.self, forKey: .account) self.status = try container.decodeIfPresent(Status.self, forKey: .status) self.emoji = try container.decodeIfPresent(String.self, forKey: .emoji) - self._emojiURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .emojiURL) ?? nil + self.emojiURL = try container.decodeIfPresent(WebURL.self, forKey: .emojiURL) } public static func dismiss(id notificationID: String) -> Request { diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/PushNotification.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/PushNotification.swift index a8456e04..178cab03 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/PushNotification.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/PushNotification.swift @@ -6,13 +6,14 @@ // import Foundation +import WebURL public struct PushNotification: Decodable { public let accessToken: String public let preferredLocale: String public let notificationID: String public let notificationType: Notification.Kind - @URLDecoder public var icon: URL + public let icon: WebURL public let title: String public let body: String @@ -28,7 +29,7 @@ public struct PushNotification: Decodable { self.notificationID = i.description } self.notificationType = try container.decode(Notification.Kind.self, forKey: .notificationType) - self._icon = try container.decode(URLDecoder.self, forKey: .icon) + self.icon = try container.decode(WebURL.self, forKey: .icon) self.title = try container.decode(String.self, forKey: .title) self.body = try container.decode(String.self, forKey: .body) } diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Status.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Status.swift index 0f5f1b82..33e26e3d 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Status.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Status.swift @@ -7,6 +7,7 @@ // import Foundation +import WebURL public final class Status: StatusProtocol, Decodable, Sendable { /// The pseudo-visibility used by instance types (Akkoma) that overload the visibility for local-only posts. @@ -14,8 +15,7 @@ public final class Status: StatusProtocol, Decodable, Sendable { public let id: String public let uri: String - private let _url: OptionalURLDecoder - public var url: URL? { _url.wrappedValue } + public let url: WebURL? public let account: Account public let inReplyToID: String? public let inReplyToAccountID: String? @@ -55,13 +55,13 @@ public final class Status: StatusProtocol, Decodable, Sendable { self.id = try container.decode(String.self, forKey: .id) self.uri = try container.decode(String.self, forKey: .uri) do { - self._url = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .url) ?? nil + self.url = try container.decodeIfPresent(WebURL.self, forKey: .url) } catch { let s = try? container.decode(String.self, forKey: .url) if s == "" { - self._url = OptionalURLDecoder(wrappedValue: nil) + self.url = nil } else { - throw error + throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath + [CodingKeys.url], debugDescription: "Could not decode URL '\(s ?? "")'", underlyingError: error)) } } self.account = try container.decode(Account.self, forKey: .account) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Utilities/NotificationGroup.swift b/Packages/Pachyderm/Sources/Pachyderm/Utilities/NotificationGroup.swift index 38a4d34e..47a9e5b1 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Utilities/NotificationGroup.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Utilities/NotificationGroup.swift @@ -7,6 +7,7 @@ // import Foundation +import WebURL public struct NotificationGroup: Identifiable, Hashable, Sendable { public private(set) var notifications: [Notification] @@ -149,7 +150,7 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable { case poll case update case status - case emojiReaction(String, URL?) + case emojiReaction(String, WebURL?) case unknown var notificationKind: Notification.Kind { diff --git a/Packages/Pachyderm/Sources/Pachyderm/Utilities/URLDecoder.swift b/Packages/Pachyderm/Sources/Pachyderm/Utilities/URLDecoder.swift deleted file mode 100644 index fff68776..00000000 --- a/Packages/Pachyderm/Sources/Pachyderm/Utilities/URLDecoder.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// URLDecoder.swift -// Pachyderm -// -// Created by Shadowfacts on 12/15/24. -// -import Foundation - -private let parseStrategy = URL.ParseStrategy() - .scheme(.required) - .user(.optional) - .password(.optional) - .host(.required) - .port(.optional) - .path(.optional) - .query(.optional) - .fragment(.optional) - -private let formatStyle = URL.FormatStyle() - .scheme(.always) - .user(.omitWhen(.user, matches: [""])) - .password(.omitWhen(.password, matches: [""])) - .host(.always) - .port(.omitIfHTTPFamily) - .path(.always) - .query(.omitWhen(.query, matches: [""])) - .fragment(.omitWhen(.fragment, matches: [""])) - -@propertyWrapper -public struct URLDecoder: Codable, Sendable, Hashable { - public var wrappedValue: URL - - public init(wrappedValue: URL) { - self.wrappedValue = wrappedValue - } - - public init(from decoder: any Decoder) throws { - let s = try decoder.singleValueContainer().decode(String.self) - self.wrappedValue = try parseStrategy.parse(s) - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(wrappedValue.formatted(formatStyle)) - } -} - -@propertyWrapper -public struct OptionalURLDecoder: Codable, Sendable, Hashable, ExpressibleByNilLiteral { - public var wrappedValue: URL? - - public init(wrappedValue: URL?) { - self.wrappedValue = wrappedValue - } - - public init(nilLiteral: ()) { - self.wrappedValue = nil - } - - public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - if container.decodeNil() { - self.wrappedValue = nil - } else { - let s = try container.decode(String.self) - if s.isEmpty { - self.wrappedValue = nil - } else { - do { - self.wrappedValue = try parseStrategy.parse(s) - } catch { - throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Could not decode URL '\(s)'", underlyingError: error)) - } - } - } - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - if let wrappedValue { - try container.encode(wrappedValue.formatted(formatStyle)) - } else { - try container.encodeNil() - } - } -} diff --git a/Packages/Pachyderm/Tests/PachydermTests/URLTests.swift b/Packages/Pachyderm/Tests/PachydermTests/URLTests.swift index 328060dc..7564c398 100644 --- a/Packages/Pachyderm/Tests/PachydermTests/URLTests.swift +++ b/Packages/Pachyderm/Tests/PachydermTests/URLTests.swift @@ -6,21 +6,20 @@ // import XCTest -@testable import Pachyderm +import WebURL +import WebURLFoundationExtras class URLTests: XCTestCase { func testDecodeURL() { - XCTAssertNotNil(try? URL.ParseStrategy().parse("https://xn--baw-joa.social/test/é")) - XCTAssertNotNil(try? URL.ParseStrategy().parse("http://見.香港/热狗/🌭")) - } - - func testRoundtripURL() throws { - let orig = URLDecoder(wrappedValue: URL(string: "https://example.com")!) - let encoded = try JSONEncoder().encode(orig) - print(String(data: encoded, encoding: .utf8)!) - let decoded = try JSONDecoder().decode(URLDecoder.self, from: encoded) - XCTAssertEqual(orig.wrappedValue, decoded.wrappedValue) + XCTAssertNotNil(WebURL(URL(string: "https://xn--baw-joa.social/@unituebingen")!)) + XCTAssertNotNil(WebURL("https://xn--baw-joa.social/@unituebingen")) + XCTAssertNotNil(URLComponents(string: "https://xn--baw-joa.social/test/é")) + XCTAssertNotNil(WebURL("https://xn--baw-joa.social/test/é")) + if #available(iOS 16.0, *) { + XCTAssertNotNil(try? URL.ParseStrategy().parse("https://xn--baw-joa.social/test/é")) + XCTAssertNotNil(try? URL.ParseStrategy().parse("http://見.香港/热狗/🌭")) + } } } diff --git a/ShareExtension/ShareHostingController.swift b/ShareExtension/ShareHostingController.swift index 4f42c5f6..dff8f3d7 100644 --- a/ShareExtension/ShareHostingController.swift +++ b/ShareExtension/ShareHostingController.swift @@ -9,6 +9,7 @@ import SwiftUI import ComposeUI import TuskerComponents +import WebURLFoundationExtras import Combine import TuskerPreferences @@ -45,7 +46,7 @@ class ShareHostingController: UIHostingController { currentAccountContainerView: { AnyView(SwitchAccountContainerView(content: $0, mastodonContextPublisher: mastodonContextPublisher)) }, replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") }, emojiImageView: { - AnyView(AsyncImage(url: $0.url) { + AnyView(AsyncImage(url: URL($0.url)!) { $0 .resizable() .aspectRatio(contentMode: .fit) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 5208f224..3e451d7c 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -103,6 +103,7 @@ D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E02BC61C6700208903 /* UserAccounts */; }; D630C3E52BC6313400208903 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E42BC6313400208903 /* Pachyderm */; }; D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D630C4222BC7842C00208903 /* HTMLStreamer */; }; + D630C4252BC7845800208903 /* WebURL in Frameworks */ = {isa = PBXBuildFile; productRef = D630C4242BC7845800208903 /* WebURL */; }; D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; }; D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; @@ -163,6 +164,7 @@ D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; }; D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; }; D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; }; + D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; }; D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; }; D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; }; D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; }; @@ -816,6 +818,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D630C4252BC7845800208903 /* WebURL in Frameworks */, D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */, D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */, D630C3E52BC6313400208903 /* Pachyderm in Frameworks */, @@ -852,6 +855,7 @@ D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */, D6552367289870790048A653 /* ScreenCorners in Frameworks */, D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */, + D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */, D63CC702290EC0B8000E19DE /* Sentry in Frameworks */, D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */, ); @@ -1777,6 +1781,7 @@ D630C3E02BC61C6700208903 /* UserAccounts */, D630C3E42BC6313400208903 /* Pachyderm */, D630C4222BC7842C00208903 /* HTMLStreamer */, + D630C4242BC7845800208903 /* WebURL */, D62220462C7EA8DF003E43B7 /* TuskerPreferences */, ); productName = NotificationExtension; @@ -1828,6 +1833,7 @@ ); name = Tusker; packageProductDependencies = ( + D6676CA427A8D0020052936B /* WebURLFoundationExtras */, D674A50827F9128D00BA03AC /* Pachyderm */, D6552366289870790048A653 /* ScreenCorners */, D63CC701290EC0B8000E19DE /* Sentry */, @@ -1954,6 +1960,7 @@ ); mainGroup = D6D4DDC3212518A000E1C4BB; packageReferences = ( + D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */, D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */, D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */, D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */, @@ -3267,6 +3274,14 @@ minimumVersion = 1.0.1; }; }; + D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/karwa/swift-url"; + requirement = { + kind = exactVersion; + version = 0.4.2; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3304,6 +3319,11 @@ package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */; productName = HTMLStreamer; }; + D630C4242BC7845800208903 /* WebURL */ = { + isa = XCSwiftPackageProductDependency; + package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */; + productName = WebURL; + }; D635237029B78A7D009ED5E7 /* TuskerComponents */ = { isa = XCSwiftPackageProductDependency; productName = TuskerComponents; @@ -3322,6 +3342,11 @@ isa = XCSwiftPackageProductDependency; productName = TTTKit; }; + D6676CA427A8D0020052936B /* WebURLFoundationExtras */ = { + isa = XCSwiftPackageProductDependency; + package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */; + productName = WebURLFoundationExtras; + }; D674A50827F9128D00BA03AC /* Pachyderm */ = { isa = XCSwiftPackageProductDependency; productName = Pachyderm; diff --git a/Tusker/CoreData/FollowedHashtag.swift b/Tusker/CoreData/FollowedHashtag.swift index b4fb4b81..22156e27 100644 --- a/Tusker/CoreData/FollowedHashtag.swift +++ b/Tusker/CoreData/FollowedHashtag.swift @@ -9,6 +9,7 @@ import Foundation import CoreData import Pachyderm +import WebURLFoundationExtras @objc(FollowedHashtag) public final class FollowedHashtag: NSManagedObject { @@ -32,6 +33,6 @@ extension FollowedHashtag { convenience init(hashtag: Hashtag, context: NSManagedObjectContext) { self.init(context: context) self.name = hashtag.name - self.url = hashtag.url + self.url = URL(hashtag.url)! } } diff --git a/Tusker/CoreData/SavedHashtag.swift b/Tusker/CoreData/SavedHashtag.swift index 446033cc..f206d456 100644 --- a/Tusker/CoreData/SavedHashtag.swift +++ b/Tusker/CoreData/SavedHashtag.swift @@ -9,6 +9,7 @@ import Foundation import CoreData import Pachyderm +import WebURLFoundationExtras import UserAccounts @objc(SavedHashtag) @@ -41,6 +42,6 @@ extension SavedHashtag { self.init(context: context) self.accountID = account.id self.name = hashtag.name - self.url = hashtag.url + self.url = URL(hashtag.url)! } } diff --git a/Tusker/CoreData/StatusMO.swift b/Tusker/CoreData/StatusMO.swift index fd183e40..364480cc 100644 --- a/Tusker/CoreData/StatusMO.swift +++ b/Tusker/CoreData/StatusMO.swift @@ -10,6 +10,7 @@ import Foundation import CoreData import Pachyderm +import WebURLFoundationExtras @objc(StatusMO) public final class StatusMO: NSManagedObject, StatusProtocol { @@ -135,7 +136,7 @@ extension StatusMO { self.sensitive = status.sensitive self.spoilerText = status.spoilerText self.uri = status.uri - self.url = status.url + self.url = status.url != nil ? URL(status.url!) : nil self.visibility = status.visibility self.poll = status.poll self.localOnly = status.localOnly ?? false diff --git a/Tusker/HTMLConverter.swift b/Tusker/HTMLConverter.swift index 68a995b1..c1975d23 100644 --- a/Tusker/HTMLConverter.swift +++ b/Tusker/HTMLConverter.swift @@ -8,7 +8,8 @@ import UIKit import HTMLStreamer -import Pachyderm +import WebURL +import WebURLFoundationExtras class HTMLConverter { @@ -44,7 +45,17 @@ extension HTMLConverter { // note: this is duplicated in NotificationExtension struct Callbacks: HTMLConversionCallbacks { static func makeURL(string: String) -> URL? { - try? URL.ParseStrategy().parse(string) + // Converting WebURL to URL is a small but non-trivial expense (since it works by + // serializing the WebURL as a string and then having Foundation parse it again), + // so, if available, use the system parser which doesn't require another round trip. + if let url = try? URL.ParseStrategy().parse(string) { + url + } else if let web = WebURL(string), + let url = URL(web) { + url + } else { + nil + } } static func elementAction(name: String, attributes: [Attribute]) -> ElementAction { diff --git a/Tusker/Screens/Announcements/AnnouncementContentTextView.swift b/Tusker/Screens/Announcements/AnnouncementContentTextView.swift index 792b1ae1..cc9d15db 100644 --- a/Tusker/Screens/Announcements/AnnouncementContentTextView.swift +++ b/Tusker/Screens/Announcements/AnnouncementContentTextView.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import WebURL class AnnouncementContentTextView: ContentTextView { @@ -29,7 +30,7 @@ class AnnouncementContentTextView: ContentTextView { override func getMention(for url: URL, text: String) -> Mention? { announcement?.mentions.first { - $0.url == url + URL($0.url) == url }.map { Mention(url: $0.url, username: $0.username, acct: $0.acct, id: $0.id) } @@ -37,7 +38,7 @@ class AnnouncementContentTextView: ContentTextView { override func getHashtag(for url: URL, text: String) -> Hashtag? { announcement?.tags.first { - $0.url == url + URL($0.url) == url } } diff --git a/Tusker/Screens/Announcements/AnnouncementListRow.swift b/Tusker/Screens/Announcements/AnnouncementListRow.swift index e62a786a..13371c20 100644 --- a/Tusker/Screens/Announcements/AnnouncementListRow.swift +++ b/Tusker/Screens/Announcements/AnnouncementListRow.swift @@ -9,6 +9,7 @@ import SwiftUI import Pachyderm import TuskerComponents +import WebURLFoundationExtras struct AnnouncementListRow: View { @Binding var announcement: Announcement @@ -115,8 +116,8 @@ struct AnnouncementListRow: View { let url: URL? let staticURL: URL? if case .custom(let emoji) = reaction { - url = emoji.url - staticURL = emoji.staticURL + url = URL(emoji.url) + staticURL = URL(emoji.staticURL) } else { url = nil staticURL = nil diff --git a/Tusker/Screens/Conversation/ConversationViewController.swift b/Tusker/Screens/Conversation/ConversationViewController.swift index 933d8c9b..53c8b92b 100644 --- a/Tusker/Screens/Conversation/ConversationViewController.swift +++ b/Tusker/Screens/Conversation/ConversationViewController.swift @@ -8,6 +8,8 @@ import UIKit import Pachyderm +import WebURL +import WebURLFoundationExtras private let mastodonRemoteStatusRegex = try! NSRegularExpression(pattern: "^/@.+@.+/\\d{18}") private func isLikelyMastodonRemoteStatus(url: URL) -> Bool { @@ -227,10 +229,10 @@ class ConversationViewController: UIViewController { let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") { effectiveURL = location } else { - effectiveURL = url.formatted(.url.fragment(.never)) + effectiveURL = WebURL(url)!.serialized(excludingFragment: true) } } else { - effectiveURL = url.formatted(.url.fragment(.never)) + effectiveURL = WebURL(url)!.serialized(excludingFragment: true) } let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true) diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index dbf0e2be..1534baa7 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import Pachyderm import CoreData +import WebURLFoundationExtras class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController { @@ -559,7 +560,10 @@ extension ExploreViewController: UICollectionViewDragDelegate { activity.displaysAuxiliaryScene = true provider = NSItemProvider(object: activity) case let .savedHashtag(hashtag): - provider = NSItemProvider(object: hashtag.url as NSURL) + guard let url = URL(hashtag.url) else { + return [] + } + provider = NSItemProvider(object: url as NSURL) if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) { activity.displaysAuxiliaryScene = true provider.registerObject(activity, visibility: .all) diff --git a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift index 49f039e2..28691fa4 100644 --- a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift +++ b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import WebURLFoundationExtras import Combine class TrendingHashtagsViewController: UIViewController, CollectionViewController { @@ -276,10 +277,11 @@ extension TrendingHashtagsViewController: UICollectionViewDelegate { extension TrendingHashtagsViewController: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { guard let item = dataSource.itemIdentifier(for: indexPath), - case let .tag(hashtag) = item else { + case let .tag(hashtag) = item, + let url = URL(hashtag.url) else { return [] } - let provider = NSItemProvider(object: hashtag.url as NSURL) + let provider = NSItemProvider(object: url as NSURL) if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) { activity.displaysAuxiliaryScene = true provider.registerObject(activity, visibility: .all) diff --git a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift index dd06dba8..3d2c22d3 100644 --- a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift +++ b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift @@ -9,6 +9,7 @@ #if !os(visionOS) import UIKit import Pachyderm +import WebURLFoundationExtras import HTMLStreamer class TrendingLinkCardCollectionViewCell: UICollectionViewCell { @@ -70,7 +71,7 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell { self.card = card self.thumbnailView.image = nil - thumbnailView.update(for: card.image, blurhash: card.blurhash) + thumbnailView.update(for: card.image.flatMap { URL($0) }, blurhash: card.blurhash) let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines) titleLabel.text = title diff --git a/Tusker/Screens/Explore/TrendingLinkCardView.swift b/Tusker/Screens/Explore/TrendingLinkCardView.swift index 4ffeabd5..906f21b1 100644 --- a/Tusker/Screens/Explore/TrendingLinkCardView.swift +++ b/Tusker/Screens/Explore/TrendingLinkCardView.swift @@ -9,6 +9,7 @@ #if os(visionOS) import SwiftUI import Pachyderm +import WebURLFoundationExtras import HTMLStreamer struct TrendingLinkCardView: View { diff --git a/Tusker/Screens/Explore/TrendingLinksViewController.swift b/Tusker/Screens/Explore/TrendingLinksViewController.swift index d390c41f..af73cbdf 100644 --- a/Tusker/Screens/Explore/TrendingLinksViewController.swift +++ b/Tusker/Screens/Explore/TrendingLinksViewController.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import WebURLFoundationExtras import SafariServices import Combine #if os(visionOS) @@ -292,19 +293,21 @@ extension TrendingLinksViewController: UICollectionViewDelegate { } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard case .link(let card) = dataSource.itemIdentifier(for: indexPath) else { + guard case .link(let card) = dataSource.itemIdentifier(for: indexPath), + let url = URL(card.url) else { return } - selected(url: card.url) + selected(url: url) } func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { guard case .link(let card) = dataSource.itemIdentifier(for: indexPath), + let url = URL(card.url), let cell = collectionView.cellForItem(at: indexPath) else { return nil } return UIContextMenuConfiguration { - let vc = SFSafariViewController(url: card.url) + let vc = SFSafariViewController(url: url) #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color #endif @@ -321,10 +324,11 @@ extension TrendingLinksViewController: UICollectionViewDelegate { extension TrendingLinksViewController: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { - guard case .link(let card) = dataSource.itemIdentifier(for: indexPath) else { + guard case .link(let card) = dataSource.itemIdentifier(for: indexPath), + let url = URL(card.url) else { return [] } - return [UIDragItem(itemProvider: NSItemProvider(object: card.url as NSURL))] + return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))] } } diff --git a/Tusker/Screens/Explore/TrendsViewController.swift b/Tusker/Screens/Explore/TrendsViewController.swift index 96223bc2..511c22b8 100644 --- a/Tusker/Screens/Explore/TrendsViewController.swift +++ b/Tusker/Screens/Explore/TrendsViewController.swift @@ -513,7 +513,9 @@ extension TrendsViewController: UICollectionViewDelegate { show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil) case let .link(card): - selected(url: card.url) + if let url = URL(card.url) { + selected(url: url) + } case let .status(id, state): selected(status: id, state: state.copy()) @@ -542,9 +544,12 @@ extension TrendsViewController: UICollectionViewDelegate { } case let .link(card): + guard let url = URL(card.url) else { + return nil + } let cell = collectionView.cellForItem(at: indexPath)! return UIContextMenuConfiguration { - let vc = SFSafariViewController(url: card.url) + let vc = SFSafariViewController(url: url) #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color #endif @@ -619,7 +624,10 @@ extension TrendsViewController: UICollectionViewDragDelegate { return [] case let .tag(hashtag): - let provider = NSItemProvider(object: hashtag.url as NSURL) + guard let url = URL(hashtag.url) else { + return [] + } + let provider = NSItemProvider(object: url as NSURL) if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) { activity.displaysAuxiliaryScene = true provider.registerObject(activity, visibility: .all) @@ -627,7 +635,10 @@ extension TrendsViewController: UICollectionViewDragDelegate { return [UIDragItem(itemProvider: provider)] case let .link(card): - return [UIDragItem(itemProvider: NSItemProvider(object: card.url as NSURL))] + guard let url = URL(card.url) else { + return [] + } + return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))] case let .status(id, _): guard let status = mastodonController.persistentContainer.status(for: id), diff --git a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift index e4378747..a4de09d0 100644 --- a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift +++ b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift @@ -8,6 +8,7 @@ import UIKit import UserAccounts +import WebURL class FastSwitchingAccountView: UIView { @@ -130,7 +131,11 @@ class FastSwitchingAccountView: UIView { private func setupAccount(account: UserAccountInfo) { usernameLabel.text = account.username - instanceLabel.text = account.instanceURL.host(percentEncoded: false) + if let domain = WebURL.Domain(account.instanceURL.host!) { + instanceLabel.text = domain.render(.uncheckedUnicodeString) + } else { + instanceLabel.text = account.instanceURL.host! + } let controller = MastodonController.getForAccount(account) avatarTask = Task { guard let account = try? await controller.getOwnAccount(), diff --git a/Tusker/Screens/Notifications/ActionNotificationGroupCollectionViewCell.swift b/Tusker/Screens/Notifications/ActionNotificationGroupCollectionViewCell.swift index 9c4e7bf4..1494c4fa 100644 --- a/Tusker/Screens/Notifications/ActionNotificationGroupCollectionViewCell.swift +++ b/Tusker/Screens/Notifications/ActionNotificationGroupCollectionViewCell.swift @@ -155,7 +155,7 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell { fetchCustomEmojiImage?.1.cancel() case .emojiReaction(let emojiOrShortcode, let url): iconImageView.image = nil - if let url, + if let url = url.flatMap({ URL($0) }), fetchCustomEmojiImage?.0 != url { fetchCustomEmojiImage?.1.cancel() let task = Task { diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index 9643aa14..acd5b01a 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -740,7 +740,7 @@ extension NotificationsCollectionViewController: UICollectionViewDragDelegate { return cell.dragItemsForBeginning(session: session) case .poll, .update: let status = group.notifications.first!.status! - let provider = NSItemProvider(object: status.url! as NSURL) + let provider = NSItemProvider(object: URL(status.url!)! as NSURL) let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: mastodonController.accountInfo!.id) activity.displaysAuxiliaryScene = true provider.registerObject(activity, visibility: .all) diff --git a/Tusker/Screens/Preferences/Appearance/MockStatusView.swift b/Tusker/Screens/Preferences/Appearance/MockStatusView.swift index 30fcc5ab..5cbd46e2 100644 --- a/Tusker/Screens/Preferences/Appearance/MockStatusView.swift +++ b/Tusker/Screens/Preferences/Appearance/MockStatusView.swift @@ -8,6 +8,7 @@ import SwiftUI import Pachyderm +import WebURL struct MockStatusView: View { @ObservedObject private var preferences = Preferences.shared @@ -135,8 +136,8 @@ private struct MockStatusCardView: UIViewRepresentable { let view = StatusCardView() view.isUserInteractionEnabled = false let card = StatusCardView.CardData( - url: URL(string: "https://vaccor.space/tusker")!, - image: URL(string: "https://vaccor.space/tusker/img/icon.png")!, + url: WebURL("https://vaccor.space/tusker")!, + image: WebURL("https://vaccor.space/tusker/img/icon.png")!, title: "Tusker", description: "Tusker is an iOS app for Mastodon" ) diff --git a/Tusker/Screens/Preferences/PrefsAccountView.swift b/Tusker/Screens/Preferences/PrefsAccountView.swift index 595ef095..8eda85ff 100644 --- a/Tusker/Screens/Preferences/PrefsAccountView.swift +++ b/Tusker/Screens/Preferences/PrefsAccountView.swift @@ -8,6 +8,7 @@ import SwiftUI import UserAccounts +import WebURL struct PrefsAccountView: View { let account: UserAccountInfo @@ -18,7 +19,12 @@ struct PrefsAccountView: View { VStack(alignment: .prefsAvatar) { Text(verbatim: account.username) .foregroundColor(.primary) - Text(verbatim: account.instanceURL.host(percentEncoded: false)!) + let instance = if let domain = WebURL.Domain(account.instanceURL.host!) { + domain.render(.uncheckedUnicodeString) + } else { + account.instanceURL.host! + } + Text(verbatim: instance) .font(.caption) .foregroundColor(.primary) } diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index c0f5224d..714769d8 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -9,6 +9,7 @@ import UIKit import Combine import Pachyderm +import WebURLFoundationExtras fileprivate let accountCell = "accountCell" fileprivate let statusCell = "statusCell" @@ -537,7 +538,7 @@ extension SearchResultsViewController: UICollectionViewDragDelegate { url = account.url activity = UserActivityManager.showProfileActivity(id: id, accountID: accountInfo.id) case .hashtag(let tag): - url = tag.url + url = URL(tag.url)! activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: accountInfo.id)! case .status(let id, _): guard let status = mastodonController.persistentContainer.status(for: id), diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListViewController.swift index afeb9290..b2ea25a3 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListViewController.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import WebURL class StatusActionAccountListViewController: UIViewController { @@ -182,7 +183,7 @@ extension StatusActionAccountListViewController { enum ActionType { case favorite case reblog - case emojiReaction(String, URL?) + case emojiReaction(String, WebURL?) init?(_ groupKind: NotificationGroup.Kind) { switch groupKind { diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index 927c4799..5b9801cb 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -9,6 +9,7 @@ import UIKit import SafariServices import Pachyderm +import WebURLFoundationExtras import SwiftUI @MainActor @@ -153,7 +154,12 @@ extension MenuActionProvider { } } - let shareSection = actionsForURL(hashtag.url, source: source) + let shareSection: [UIMenuElement] + if let url = URL(hashtag.url) { + shareSection = actionsForURL(url, source: source) + } else { + shareSection = [] + } return [ UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), @@ -369,11 +375,14 @@ extension MenuActionProvider { } func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] { + guard let url = URL(card.url) else { + return [] + } return [ - openInSafariAction(url: card.url), + openInSafariAction(url: url), createAction(identifier: "share", title: "Share…", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in guard let self else { return } - self.navigationDelegate?.showMoreOptions(forURL: card.url, source: source) + self.navigationDelegate?.showMoreOptions(forURL: url, source: source) }), createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in guard let self = self else { return } @@ -384,7 +393,7 @@ extension MenuActionProvider { text += title text += ":\n" } - text += card.url.absoluteString + text += url.absoluteString let draft = self.mastodonController!.createDraft(text: text) self.navigationDelegate?.compose(editing: draft) diff --git a/Tusker/Views/AccountDisplayNameView.swift b/Tusker/Views/AccountDisplayNameView.swift index 7b58395e..2cda0c0f 100644 --- a/Tusker/Views/AccountDisplayNameView.swift +++ b/Tusker/Views/AccountDisplayNameView.swift @@ -8,6 +8,7 @@ import SwiftUI import Pachyderm +import WebURLFoundationExtras import os private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) @@ -53,7 +54,7 @@ struct AccountDisplayNameView: View { } group.enter() - let request = ImageCache.emojis.get(emoji.url) { (_, image) in + let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in defer { group.leave() } guard let image = image else { return } diff --git a/Tusker/Views/BaseEmojiLabel.swift b/Tusker/Views/BaseEmojiLabel.swift index 16267912..92b0d6e4 100644 --- a/Tusker/Views/BaseEmojiLabel.swift +++ b/Tusker/Views/BaseEmojiLabel.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import WebURLFoundationExtras import os private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) @@ -72,7 +73,7 @@ extension BaseEmojiLabel { foundEmojis = true - if let image = ImageCache.emojis.get(emoji.url)?.image { + if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image { // if the image is cached, add it immediately. // we generate the thumbnail on the main thread, because it's usually fast enough // and the delay caused by doing it asynchronously looks works. @@ -89,7 +90,7 @@ extension BaseEmojiLabel { // otherwise, perform the network request group.enter() - let request = ImageCache.emojis.getFromSource(emoji.url) { (_, image) in + let request = ImageCache.emojis.getFromSource(URL(emoji.url)!) { (_, image) in guard let image else { group.leave() return @@ -97,7 +98,7 @@ extension BaseEmojiLabel { image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in guard let thumbnail = thumbnail?.cgImage, case let rescaled = UIImage(cgImage: thumbnail, scale: screenScale, orientation: .up), - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: rescaled) else { + let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else { group.leave() return } diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 41230698..d2a2e504 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -9,6 +9,8 @@ import UIKit import Pachyderm import SafariServices +import WebURL +import WebURLFoundationExtras import Combine private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) diff --git a/Tusker/Views/CustomEmojiImageView.swift b/Tusker/Views/CustomEmojiImageView.swift index ae784df9..9155696d 100644 --- a/Tusker/Views/CustomEmojiImageView.swift +++ b/Tusker/Views/CustomEmojiImageView.swift @@ -8,6 +8,7 @@ import SwiftUI import Pachyderm +import WebURLFoundationExtras struct CustomEmojiImageView: View { let emoji: Emoji @@ -34,7 +35,7 @@ struct CustomEmojiImageView: View { @MainActor private func loadImage() { - request = ImageCache.emojis.get(emoji.url) { (_, image) in + request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in DispatchQueue.main.async { self.request = nil if let image = image { diff --git a/Tusker/Views/Status/StatusCardView.swift b/Tusker/Views/Status/StatusCardView.swift index 40f8ee44..a8feb6bd 100644 --- a/Tusker/Views/Status/StatusCardView.swift +++ b/Tusker/Views/Status/StatusCardView.swift @@ -9,6 +9,8 @@ import UIKit import Pachyderm import SafariServices +import WebURL +import WebURLFoundationExtras import HTMLStreamer class StatusCardView: UIView { @@ -182,14 +184,14 @@ class StatusCardView: UIView { if sensitive { if let blurhash = card.blurhash { imageView.blurImage = false - imageView.showOnlyBlurHash(blurhash, for: image) + imageView.showOnlyBlurHash(blurhash, for: URL(image)!) } else { // if we don't have a blurhash, load the image and show it behind a blur imageView.blurImage = true - imageView.update(for: image, blurhash: nil) + imageView.update(for: URL(image), blurhash: nil) } } else { - imageView.update(for: image, blurhash: card.blurhash) + imageView.update(for: URL(image), blurhash: card.blurhash) } imageView.isHidden = false leadingSpacer.isHidden = true @@ -208,8 +210,8 @@ class StatusCardView: UIView { descriptionLabel.text = description descriptionLabel.isHidden = description.isEmpty - if let host = card.url.host(percentEncoded: false) { - domainLabel.text = host + if let host = card.url.host { + domainLabel.text = host.serialized domainLabel.isHidden = false } else { domainLabel.isHidden = true @@ -236,7 +238,7 @@ class StatusCardView: UIView { setNeedsDisplay() if let card = card, let delegate = navigationDelegate { - delegate.selected(url: card.url) + delegate.selected(url: URL(card.url)!) } } @@ -246,8 +248,8 @@ class StatusCardView: UIView { } struct CardData: Equatable { - let url: URL - let image: URL? + let url: WebURL + let image: WebURL? let title: String let description: String let blurhash: String? @@ -260,7 +262,7 @@ class StatusCardView: UIView { self.blurhash = card.blurhash } - init(url: URL, image: URL? = nil, title: String, description: String, blurhash: String? = nil) { + init(url: WebURL, image: WebURL? = nil, title: String, description: String, blurhash: String? = nil) { self.url = url self.image = image self.title = title @@ -276,13 +278,13 @@ extension StatusCardView: UIContextMenuInteractionDelegate { guard let card = card else { return nil } return UIContextMenuConfiguration(identifier: nil) { - let vc = SFSafariViewController(url: card.url) + let vc = SFSafariViewController(url: URL(card.url)!) #if !os(visionOS) vc.preferredControlTintColor = Preferences.shared.accentColor.color #endif return vc } actionProvider: { (_) in - let actions = self.actionProvider?.actionsForURL(card.url, source: .view(self)) ?? [] + let actions = self.actionProvider?.actionsForURL(URL(card.url)!, source: .view(self)) ?? [] return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions) } } diff --git a/Tusker/Views/StatusContentTextView.swift b/Tusker/Views/StatusContentTextView.swift index d7458c51..1fdaee45 100644 --- a/Tusker/Views/StatusContentTextView.swift +++ b/Tusker/Views/StatusContentTextView.swift @@ -8,6 +8,7 @@ import UIKit import Pachyderm +import WebURLFoundationExtras class StatusContentTextView: ContentTextView { @@ -25,7 +26,7 @@ class StatusContentTextView: ContentTextView { let mastodonController = mastodonController, let status = mastodonController.persistentContainer.status(for: statusID) { mention = status.mentions.first { (mention) in - url.host() == mention.url.host() && ( + url.host == mention.url.host!.serialized && ( text.dropFirst() == mention.username // Mastodon and Pleroma include @ in the text || text.dropFirst() == mention.acct // Misskey includes @ and uses the whole acct || text == mention.username // GNU Social does not include the @ in the text, so we don't need to drop it @@ -43,7 +44,7 @@ class StatusContentTextView: ContentTextView { let mastodonController = mastodonController, let status = mastodonController.persistentContainer.status(for: statusID) { hashtag = status.hashtags.first { (hashtag) in - hashtag.url == url + URL(hashtag.url) == url } } else { hashtag = nil