Replace WebURL with URL.ParseStrategy

Closes #170
This commit is contained in:
Shadowfacts 2024-12-15 14:20:48 -05:00
parent 572c5a0824
commit adaf8dc217
43 changed files with 187 additions and 238 deletions

View File

@ -14,7 +14,6 @@ import OSLog
import Pachyderm
import Intents
import HTMLStreamer
import WebURL
import UIKit
import TuskerPreferences
@ -238,8 +237,7 @@ 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 url = URL(emoji.url),
let (data, _) = try? await URLSession.shared.data(from: url),
let (data, _) = try? await URLSession.shared.data(from: emoji.url),
let image = UIImage(data: data) else {
continue
}
@ -368,17 +366,7 @@ private func decodeBase64URL(_ s: String) -> Data? {
// copied from HTMLConverter.Callbacks, blergh
private struct HTMLCallbacks: HTMLConversionCallbacks {
static func makeURL(string: String) -> URL? {
// 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
}
try? URL.ParseStrategy().parse(string)
}
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {

View File

@ -7,6 +7,7 @@ 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.
@ -16,7 +17,6 @@ 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,8 +24,6 @@ let package = Package(
.target(
name: "Pachyderm",
dependencies: [
.product(name: "WebURL", package: "swift-url"),
.product(name: "WebURLFoundationExtras", package: "swift-url"),
],
swiftSettings: [
.swiftLanguageMode(.v5)

View File

@ -7,7 +7,6 @@
//
import Foundation
import WebURL
/**
The base Mastodon API client.
@ -202,8 +201,8 @@ public struct Client: Sendable {
let wellKnown = Request<WellKnown>(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 = WebURL(url.href),
href.host == WebURL(self.baseURL)?.host {
let href = try? URL.ParseStrategy().parse(url.href),
href.host == self.baseURL.host() {
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
return try await run(nodeInfo).0
} else {

View File

@ -6,7 +6,6 @@
//
import Foundation
import WebURL
public struct Announcement: Decodable, Sendable, Hashable, Identifiable {
public let id: String
@ -60,7 +59,7 @@ extension Announcement {
public struct Account: Decodable, Sendable, Hashable {
public let id: String
public let username: String
public let url: WebURL
@URLDecoder public var url: URL
public let acct: String
}
}
@ -68,7 +67,7 @@ extension Announcement {
extension Announcement {
public struct Status: Decodable, Sendable, Hashable {
public let id: String
public let url: WebURL
@URLDecoder public var url: URL
}
}

View File

@ -7,18 +7,17 @@
//
import Foundation
import WebURL
public struct Card: Codable, Sendable {
public let url: WebURL
@URLDecoder public var url: URL
public let title: String
public let description: String
public let image: WebURL?
@OptionalURLDecoder public var image: URL?
public let kind: Kind
public let authorName: String?
public let authorURL: WebURL?
@OptionalURLDecoder public var authorURL: URL?
public let providerName: String?
public let providerURL: WebURL?
@OptionalURLDecoder public var providerURL: URL?
public let html: String?
public let width: Int?
public let height: Int?
@ -27,15 +26,15 @@ public struct Card: Codable, Sendable {
public let history: [History]?
public init(
url: WebURL,
url: URL,
title: String,
description: String,
image: WebURL? = nil,
image: URL? = nil,
kind: Card.Kind,
authorName: String? = nil,
authorURL: WebURL? = nil,
authorURL: URL? = nil,
providerName: String? = nil,
providerURL: WebURL? = nil,
providerURL: URL? = nil,
html: String? = nil,
width: Int? = nil,
height: Int? = nil,
@ -61,15 +60,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(WebURL.self, forKey: .url)
self._url = try container.decode(URLDecoder.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(WebURL.self, forKey: .image)
self._image = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .image) ?? nil
self.authorName = try? container.decodeIfPresent(String.self, forKey: .authorName)
self.authorURL = try? container.decodeIfPresent(WebURL.self, forKey: .authorURL)
self._authorURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .authorURL) ?? nil
self.providerName = try? container.decodeIfPresent(String.self, forKey: .providerName)
self.providerURL = try? container.decodeIfPresent(WebURL.self, forKey: .providerURL)
self._providerURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .providerURL) ?? nil
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)

View File

@ -7,14 +7,11 @@
//
import Foundation
import WebURL
public struct Emoji: Codable, Sendable {
public let shortcode: String
// 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
@URLDecoder public var url: URL
@URLDecoder public var staticURL: URL
public let visibleInPicker: Bool
public let category: String?
@ -22,13 +19,8 @@ public struct Emoji: Codable, Sendable {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.shortcode = try container.decode(String.self, forKey: .shortcode)
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 ?? "<failed to decode string>")'", underlyingError: error))
}
self.staticURL = try container.decode(WebURL.self, forKey: .staticURL)
self._url = try container.decode(URLDecoder.self, forKey: .url)
self._staticURL = try container.decode(URLDecoder.self, forKey: .staticURL)
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
self.category = try container.decodeIfPresent(String.self, forKey: .category)
}

View File

@ -7,12 +7,10 @@
//
import Foundation
import WebURL
import WebURLFoundationExtras
public struct Hashtag: Codable, Sendable {
public let name: String
public let url: WebURL
@URLDecoder public var url: URL
/// Only present when returned from the trending hashtags endpoint
public let history: [History]?
/// Only present on Mastodon >= 4 and when logged in
@ -20,7 +18,7 @@ public struct Hashtag: Codable, Sendable {
public init(name: String, url: URL) {
self.name = name
self.url = WebURL(url)!
self.url = url
self.history = nil
self.following = nil
}
@ -29,7 +27,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(WebURL.self, forKey: .url)
self._url = try container.decode(URLDecoder.self, forKey: .url)
self.history = try container.decodeIfPresent([History].self, forKey: .history)
self.following = try container.decodeIfPresent(Bool.self, forKey: .following)
}

View File

@ -7,10 +7,9 @@
//
import Foundation
import WebURL
public struct Mention: Codable, Sendable {
public let url: WebURL
@URLDecoder public var url: URL
public let username: String
public let acct: String
/// The instance-local ID of the user being mentioned.
@ -21,15 +20,10 @@ 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)
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 ?? "<failed to decode string>")'")
}
self._url = try container.decode(URLDecoder.self, forKey: .url)
}
public init(url: WebURL, username: String, acct: String, id: String) {
public init(url: URL, username: String, acct: String, id: String) {
self.url = url
self.username = username
self.acct = acct

View File

@ -7,7 +7,6 @@
//
import Foundation
import WebURL
public struct Notification: Decodable, Sendable {
public let id: String
@ -18,7 +17,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?
public let emojiURL: WebURL?
@OptionalURLDecoder public var emojiURL: URL?
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -33,7 +32,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(WebURL.self, forKey: .emojiURL)
self._emojiURL = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .emojiURL) ?? nil
}
public static func dismiss(id notificationID: String) -> Request<Empty> {

View File

@ -6,14 +6,13 @@
//
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
public let icon: WebURL
@URLDecoder public var icon: URL
public let title: String
public let body: String
@ -29,7 +28,7 @@ public struct PushNotification: Decodable {
self.notificationID = i.description
}
self.notificationType = try container.decode(Notification.Kind.self, forKey: .notificationType)
self.icon = try container.decode(WebURL.self, forKey: .icon)
self._icon = try container.decode(URLDecoder.self, forKey: .icon)
self.title = try container.decode(String.self, forKey: .title)
self.body = try container.decode(String.self, forKey: .body)
}

View File

@ -7,7 +7,6 @@
//
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.
@ -15,7 +14,8 @@ public final class Status: StatusProtocol, Decodable, Sendable {
public let id: String
public let uri: String
public let url: WebURL?
private let _url: OptionalURLDecoder
public var url: URL? { _url.wrappedValue }
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(WebURL.self, forKey: .url)
self._url = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .url) ?? nil
} catch {
let s = try? container.decode(String.self, forKey: .url)
if s == "" {
self.url = nil
self._url = OptionalURLDecoder(wrappedValue: nil)
} else {
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath + [CodingKeys.url], debugDescription: "Could not decode URL '\(s ?? "<failed to decode string>")'", underlyingError: error))
throw error
}
}
self.account = try container.decode(Account.self, forKey: .account)

View File

@ -7,7 +7,6 @@
//
import Foundation
import WebURL
public struct NotificationGroup: Identifiable, Hashable, Sendable {
public private(set) var notifications: [Notification]
@ -150,7 +149,7 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable {
case poll
case update
case status
case emojiReaction(String, WebURL?)
case emojiReaction(String, URL?)
case unknown
var notificationKind: Notification.Kind {

View File

@ -0,0 +1,82 @@
//
// 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)
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()
}
}
}

View File

@ -6,20 +6,21 @@
//
import XCTest
import WebURL
import WebURLFoundationExtras
@testable import Pachyderm
class URLTests: XCTestCase {
func testDecodeURL() {
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://見.香港/热狗/🌭"))
}
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)
}
}

View File

@ -9,7 +9,6 @@
import SwiftUI
import ComposeUI
import TuskerComponents
import WebURLFoundationExtras
import Combine
import TuskerPreferences
@ -46,7 +45,7 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
currentAccountContainerView: { AnyView(SwitchAccountContainerView(content: $0, mastodonContextPublisher: mastodonContextPublisher)) },
replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") },
emojiImageView: {
AnyView(AsyncImage(url: URL($0.url)!) {
AnyView(AsyncImage(url: $0.url) {
$0
.resizable()
.aspectRatio(contentMode: .fit)

View File

@ -103,7 +103,6 @@
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 */; };
@ -164,7 +163,6 @@
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 */; };
@ -818,7 +816,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D630C4252BC7845800208903 /* WebURL in Frameworks */,
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
@ -855,7 +852,6 @@
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */,
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
);
@ -1781,7 +1777,6 @@
D630C3E02BC61C6700208903 /* UserAccounts */,
D630C3E42BC6313400208903 /* Pachyderm */,
D630C4222BC7842C00208903 /* HTMLStreamer */,
D630C4242BC7845800208903 /* WebURL */,
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
);
productName = NotificationExtension;
@ -1833,7 +1828,6 @@
);
name = Tusker;
packageProductDependencies = (
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
D674A50827F9128D00BA03AC /* Pachyderm */,
D6552366289870790048A653 /* ScreenCorners */,
D63CC701290EC0B8000E19DE /* Sentry */,
@ -1960,7 +1954,6 @@
);
mainGroup = D6D4DDC3212518A000E1C4BB;
packageReferences = (
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
@ -3274,14 +3267,6 @@
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 */
@ -3319,11 +3304,6 @@
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
productName = HTMLStreamer;
};
D630C4242BC7845800208903 /* WebURL */ = {
isa = XCSwiftPackageProductDependency;
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
productName = WebURL;
};
D635237029B78A7D009ED5E7 /* TuskerComponents */ = {
isa = XCSwiftPackageProductDependency;
productName = TuskerComponents;
@ -3342,11 +3322,6 @@
isa = XCSwiftPackageProductDependency;
productName = TTTKit;
};
D6676CA427A8D0020052936B /* WebURLFoundationExtras */ = {
isa = XCSwiftPackageProductDependency;
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
productName = WebURLFoundationExtras;
};
D674A50827F9128D00BA03AC /* Pachyderm */ = {
isa = XCSwiftPackageProductDependency;
productName = Pachyderm;

View File

@ -9,7 +9,6 @@
import Foundation
import CoreData
import Pachyderm
import WebURLFoundationExtras
@objc(FollowedHashtag)
public final class FollowedHashtag: NSManagedObject {
@ -33,6 +32,6 @@ extension FollowedHashtag {
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
self.init(context: context)
self.name = hashtag.name
self.url = URL(hashtag.url)!
self.url = hashtag.url
}
}

View File

@ -9,7 +9,6 @@
import Foundation
import CoreData
import Pachyderm
import WebURLFoundationExtras
import UserAccounts
@objc(SavedHashtag)
@ -42,6 +41,6 @@ extension SavedHashtag {
self.init(context: context)
self.accountID = account.id
self.name = hashtag.name
self.url = URL(hashtag.url)!
self.url = hashtag.url
}
}

View File

@ -10,7 +10,6 @@
import Foundation
import CoreData
import Pachyderm
import WebURLFoundationExtras
@objc(StatusMO)
public final class StatusMO: NSManagedObject, StatusProtocol {
@ -136,7 +135,7 @@ extension StatusMO {
self.sensitive = status.sensitive
self.spoilerText = status.spoilerText
self.uri = status.uri
self.url = status.url != nil ? URL(status.url!) : nil
self.url = status.url
self.visibility = status.visibility
self.poll = status.poll
self.localOnly = status.localOnly ?? false

View File

@ -8,8 +8,7 @@
import UIKit
import HTMLStreamer
import WebURL
import WebURLFoundationExtras
import Pachyderm
class HTMLConverter {
@ -45,17 +44,7 @@ extension HTMLConverter {
// note: this is duplicated in NotificationExtension
struct Callbacks: HTMLConversionCallbacks {
static func makeURL(string: String) -> URL? {
// 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
}
try? URL.ParseStrategy().parse(string)
}
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {

View File

@ -8,7 +8,6 @@
import UIKit
import Pachyderm
import WebURL
class AnnouncementContentTextView: ContentTextView {
@ -30,7 +29,7 @@ class AnnouncementContentTextView: ContentTextView {
override func getMention(for url: URL, text: String) -> Mention? {
announcement?.mentions.first {
URL($0.url) == url
$0.url == url
}.map {
Mention(url: $0.url, username: $0.username, acct: $0.acct, id: $0.id)
}
@ -38,7 +37,7 @@ class AnnouncementContentTextView: ContentTextView {
override func getHashtag(for url: URL, text: String) -> Hashtag? {
announcement?.tags.first {
URL($0.url) == url
$0.url == url
}
}

View File

@ -9,7 +9,6 @@
import SwiftUI
import Pachyderm
import TuskerComponents
import WebURLFoundationExtras
struct AnnouncementListRow: View {
@Binding var announcement: Announcement
@ -116,8 +115,8 @@ struct AnnouncementListRow: View {
let url: URL?
let staticURL: URL?
if case .custom(let emoji) = reaction {
url = URL(emoji.url)
staticURL = URL(emoji.staticURL)
url = emoji.url
staticURL = emoji.staticURL
} else {
url = nil
staticURL = nil

View File

@ -8,8 +8,6 @@
import UIKit
import Pachyderm
import WebURL
import WebURLFoundationExtras
private let mastodonRemoteStatusRegex = try! NSRegularExpression(pattern: "^/@.+@.+/\\d{18}")
private func isLikelyMastodonRemoteStatus(url: URL) -> Bool {
@ -229,10 +227,10 @@ class ConversationViewController: UIViewController {
let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") {
effectiveURL = location
} else {
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
effectiveURL = url.formatted(.url.fragment(.never))
}
} else {
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
effectiveURL = url.formatted(.url.fragment(.never))
}
let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)

View File

@ -10,7 +10,6 @@ import UIKit
import Combine
import Pachyderm
import CoreData
import WebURLFoundationExtras
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
@ -560,10 +559,7 @@ extension ExploreViewController: UICollectionViewDragDelegate {
activity.displaysAuxiliaryScene = true
provider = NSItemProvider(object: activity)
case let .savedHashtag(hashtag):
guard let url = URL(hashtag.url) else {
return []
}
provider = NSItemProvider(object: url as NSURL)
provider = NSItemProvider(object: hashtag.url as NSURL)
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)

View File

@ -8,7 +8,6 @@
import UIKit
import Pachyderm
import WebURLFoundationExtras
import Combine
class TrendingHashtagsViewController: UIViewController, CollectionViewController {
@ -277,11 +276,10 @@ 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,
let url = URL(hashtag.url) else {
case let .tag(hashtag) = item else {
return []
}
let provider = NSItemProvider(object: url as NSURL)
let provider = NSItemProvider(object: hashtag.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)

View File

@ -9,7 +9,6 @@
#if !os(visionOS)
import UIKit
import Pachyderm
import WebURLFoundationExtras
import HTMLStreamer
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
@ -71,7 +70,7 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
self.card = card
self.thumbnailView.image = nil
thumbnailView.update(for: card.image.flatMap { URL($0) }, blurhash: card.blurhash)
thumbnailView.update(for: card.image, blurhash: card.blurhash)
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title

View File

@ -9,7 +9,6 @@
#if os(visionOS)
import SwiftUI
import Pachyderm
import WebURLFoundationExtras
import HTMLStreamer
struct TrendingLinkCardView: View {

View File

@ -8,7 +8,6 @@
import UIKit
import Pachyderm
import WebURLFoundationExtras
import SafariServices
import Combine
#if os(visionOS)
@ -293,21 +292,19 @@ extension TrendingLinksViewController: UICollectionViewDelegate {
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
let url = URL(card.url) else {
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath) else {
return
}
selected(url: url)
selected(url: card.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: url)
let vc = SFSafariViewController(url: card.url)
#if !os(visionOS)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
#endif
@ -324,11 +321,10 @@ 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),
let url = URL(card.url) else {
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath) else {
return []
}
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
return [UIDragItem(itemProvider: NSItemProvider(object: card.url as NSURL))]
}
}

View File

@ -513,9 +513,7 @@ extension TrendsViewController: UICollectionViewDelegate {
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
case let .link(card):
if let url = URL(card.url) {
selected(url: url)
}
selected(url: card.url)
case let .status(id, state):
selected(status: id, state: state.copy())
@ -544,12 +542,9 @@ 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: url)
let vc = SFSafariViewController(url: card.url)
#if !os(visionOS)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
#endif
@ -624,10 +619,7 @@ extension TrendsViewController: UICollectionViewDragDelegate {
return []
case let .tag(hashtag):
guard let url = URL(hashtag.url) else {
return []
}
let provider = NSItemProvider(object: url as NSURL)
let provider = NSItemProvider(object: hashtag.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)
@ -635,10 +627,7 @@ extension TrendsViewController: UICollectionViewDragDelegate {
return [UIDragItem(itemProvider: provider)]
case let .link(card):
guard let url = URL(card.url) else {
return []
}
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
return [UIDragItem(itemProvider: NSItemProvider(object: card.url as NSURL))]
case let .status(id, _):
guard let status = mastodonController.persistentContainer.status(for: id),

View File

@ -8,7 +8,6 @@
import UIKit
import UserAccounts
import WebURL
class FastSwitchingAccountView: UIView {
@ -131,11 +130,7 @@ class FastSwitchingAccountView: UIView {
private func setupAccount(account: UserAccountInfo) {
usernameLabel.text = account.username
if let domain = WebURL.Domain(account.instanceURL.host!) {
instanceLabel.text = domain.render(.uncheckedUnicodeString)
} else {
instanceLabel.text = account.instanceURL.host!
}
instanceLabel.text = account.instanceURL.host(percentEncoded: false)
let controller = MastodonController.getForAccount(account)
avatarTask = Task {
guard let account = try? await controller.getOwnAccount(),

View File

@ -155,7 +155,7 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
fetchCustomEmojiImage?.1.cancel()
case .emojiReaction(let emojiOrShortcode, let url):
iconImageView.image = nil
if let url = url.flatMap({ URL($0) }),
if let url,
fetchCustomEmojiImage?.0 != url {
fetchCustomEmojiImage?.1.cancel()
let task = Task {

View File

@ -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: URL(status.url!)! as NSURL)
let provider = NSItemProvider(object: status.url! as NSURL)
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: mastodonController.accountInfo!.id)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)

View File

@ -8,7 +8,6 @@
import SwiftUI
import Pachyderm
import WebURL
struct MockStatusView: View {
@ObservedObject private var preferences = Preferences.shared
@ -136,8 +135,8 @@ private struct MockStatusCardView: UIViewRepresentable {
let view = StatusCardView()
view.isUserInteractionEnabled = false
let card = StatusCardView.CardData(
url: WebURL("https://vaccor.space/tusker")!,
image: WebURL("https://vaccor.space/tusker/img/icon.png")!,
url: URL(string: "https://vaccor.space/tusker")!,
image: URL(string: "https://vaccor.space/tusker/img/icon.png")!,
title: "Tusker",
description: "Tusker is an iOS app for Mastodon"
)

View File

@ -8,7 +8,6 @@
import SwiftUI
import UserAccounts
import WebURL
struct PrefsAccountView: View {
let account: UserAccountInfo
@ -19,12 +18,7 @@ struct PrefsAccountView: View {
VStack(alignment: .prefsAvatar) {
Text(verbatim: account.username)
.foregroundColor(.primary)
let instance = if let domain = WebURL.Domain(account.instanceURL.host!) {
domain.render(.uncheckedUnicodeString)
} else {
account.instanceURL.host!
}
Text(verbatim: instance)
Text(verbatim: account.instanceURL.host(percentEncoded: false)!)
.font(.caption)
.foregroundColor(.primary)
}

View File

@ -9,7 +9,6 @@
import UIKit
import Combine
import Pachyderm
import WebURLFoundationExtras
fileprivate let accountCell = "accountCell"
fileprivate let statusCell = "statusCell"
@ -538,7 +537,7 @@ extension SearchResultsViewController: UICollectionViewDragDelegate {
url = account.url
activity = UserActivityManager.showProfileActivity(id: id, accountID: accountInfo.id)
case .hashtag(let tag):
url = URL(tag.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),

View File

@ -8,7 +8,6 @@
import UIKit
import Pachyderm
import WebURL
class StatusActionAccountListViewController: UIViewController {
@ -183,7 +182,7 @@ extension StatusActionAccountListViewController {
enum ActionType {
case favorite
case reblog
case emojiReaction(String, WebURL?)
case emojiReaction(String, URL?)
init?(_ groupKind: NotificationGroup.Kind) {
switch groupKind {

View File

@ -9,7 +9,6 @@
import UIKit
import SafariServices
import Pachyderm
import WebURLFoundationExtras
import SwiftUI
@MainActor
@ -154,12 +153,7 @@ extension MenuActionProvider {
}
}
let shareSection: [UIMenuElement]
if let url = URL(hashtag.url) {
shareSection = actionsForURL(url, source: source)
} else {
shareSection = []
}
let shareSection = actionsForURL(hashtag.url, source: source)
return [
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
@ -375,14 +369,11 @@ extension MenuActionProvider {
}
func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {
guard let url = URL(card.url) else {
return []
}
return [
openInSafariAction(url: url),
openInSafariAction(url: card.url),
createAction(identifier: "share", title: "Share…", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
guard let self else { return }
self.navigationDelegate?.showMoreOptions(forURL: url, source: source)
self.navigationDelegate?.showMoreOptions(forURL: card.url, source: source)
}),
createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in
guard let self = self else { return }
@ -393,7 +384,7 @@ extension MenuActionProvider {
text += title
text += ":\n"
}
text += url.absoluteString
text += card.url.absoluteString
let draft = self.mastodonController!.createDraft(text: text)
self.navigationDelegate?.compose(editing: draft)

View File

@ -8,7 +8,6 @@
import SwiftUI
import Pachyderm
import WebURLFoundationExtras
import os
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
@ -54,7 +53,7 @@ struct AccountDisplayNameView: View {
}
group.enter()
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
defer { group.leave() }
guard let image = image else { return }

View File

@ -8,7 +8,6 @@
import UIKit
import Pachyderm
import WebURLFoundationExtras
import os
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
@ -73,7 +72,7 @@ extension BaseEmojiLabel {
foundEmojis = true
if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image {
if let image = ImageCache.emojis.get(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.
@ -90,7 +89,7 @@ extension BaseEmojiLabel {
// otherwise, perform the network request
group.enter()
let request = ImageCache.emojis.getFromSource(URL(emoji.url)!) { (_, image) in
let request = ImageCache.emojis.getFromSource(emoji.url) { (_, image) in
guard let image else {
group.leave()
return
@ -98,7 +97,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: URL(emoji.url)!, image: rescaled) else {
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: emoji.url, image: rescaled) else {
group.leave()
return
}

View File

@ -9,8 +9,6 @@
import UIKit
import Pachyderm
import SafariServices
import WebURL
import WebURLFoundationExtras
import Combine
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])

View File

@ -8,7 +8,6 @@
import SwiftUI
import Pachyderm
import WebURLFoundationExtras
struct CustomEmojiImageView: View {
let emoji: Emoji
@ -35,7 +34,7 @@ struct CustomEmojiImageView: View {
@MainActor
private func loadImage() {
request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
request = ImageCache.emojis.get(emoji.url) { (_, image) in
DispatchQueue.main.async {
self.request = nil
if let image = image {

View File

@ -9,8 +9,6 @@
import UIKit
import Pachyderm
import SafariServices
import WebURL
import WebURLFoundationExtras
import HTMLStreamer
class StatusCardView: UIView {
@ -184,14 +182,14 @@ class StatusCardView: UIView {
if sensitive {
if let blurhash = card.blurhash {
imageView.blurImage = false
imageView.showOnlyBlurHash(blurhash, for: URL(image)!)
imageView.showOnlyBlurHash(blurhash, for: image)
} else {
// if we don't have a blurhash, load the image and show it behind a blur
imageView.blurImage = true
imageView.update(for: URL(image), blurhash: nil)
imageView.update(for: image, blurhash: nil)
}
} else {
imageView.update(for: URL(image), blurhash: card.blurhash)
imageView.update(for: image, blurhash: card.blurhash)
}
imageView.isHidden = false
leadingSpacer.isHidden = true
@ -210,8 +208,8 @@ class StatusCardView: UIView {
descriptionLabel.text = description
descriptionLabel.isHidden = description.isEmpty
if let host = card.url.host {
domainLabel.text = host.serialized
if let host = card.url.host(percentEncoded: false) {
domainLabel.text = host
domainLabel.isHidden = false
} else {
domainLabel.isHidden = true
@ -238,7 +236,7 @@ class StatusCardView: UIView {
setNeedsDisplay()
if let card = card, let delegate = navigationDelegate {
delegate.selected(url: URL(card.url)!)
delegate.selected(url: card.url)
}
}
@ -248,8 +246,8 @@ class StatusCardView: UIView {
}
struct CardData: Equatable {
let url: WebURL
let image: WebURL?
let url: URL
let image: URL?
let title: String
let description: String
let blurhash: String?
@ -262,7 +260,7 @@ class StatusCardView: UIView {
self.blurhash = card.blurhash
}
init(url: WebURL, image: WebURL? = nil, title: String, description: String, blurhash: String? = nil) {
init(url: URL, image: URL? = nil, title: String, description: String, blurhash: String? = nil) {
self.url = url
self.image = image
self.title = title
@ -278,13 +276,13 @@ extension StatusCardView: UIContextMenuInteractionDelegate {
guard let card = card else { return nil }
return UIContextMenuConfiguration(identifier: nil) {
let vc = SFSafariViewController(url: URL(card.url)!)
let vc = SFSafariViewController(url: card.url)
#if !os(visionOS)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
#endif
return vc
} actionProvider: { (_) in
let actions = self.actionProvider?.actionsForURL(URL(card.url)!, source: .view(self)) ?? []
let actions = self.actionProvider?.actionsForURL(card.url, source: .view(self)) ?? []
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
}
}

View File

@ -8,7 +8,6 @@
import UIKit
import Pachyderm
import WebURLFoundationExtras
class StatusContentTextView: ContentTextView {
@ -26,7 +25,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!.serialized && (
url.host() == mention.url.host() && (
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
@ -44,7 +43,7 @@ class StatusContentTextView: ContentTextView {
let mastodonController = mastodonController,
let status = mastodonController.persistentContainer.status(for: statusID) {
hashtag = status.hashtags.first { (hashtag) in
URL(hashtag.url) == url
hashtag.url == url
}
} else {
hashtag = nil