forked from shadowfacts/Tusker
parent
572c5a0824
commit
adaf8dc217
|
@ -14,7 +14,6 @@ import OSLog
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Intents
|
import Intents
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
import WebURL
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import TuskerPreferences
|
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() {
|
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))
|
let emojiName = (content.body as NSString).substring(with: match.range(at: 1))
|
||||||
guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }),
|
guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }),
|
||||||
let url = URL(emoji.url),
|
let (data, _) = try? await URLSession.shared.data(from: emoji.url),
|
||||||
let (data, _) = try? await URLSession.shared.data(from: url),
|
|
||||||
let image = UIImage(data: data) else {
|
let image = UIImage(data: data) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -368,17 +366,7 @@ private func decodeBase64URL(_ s: String) -> Data? {
|
||||||
// copied from HTMLConverter.Callbacks, blergh
|
// copied from HTMLConverter.Callbacks, blergh
|
||||||
private struct HTMLCallbacks: HTMLConversionCallbacks {
|
private struct HTMLCallbacks: HTMLConversionCallbacks {
|
||||||
static func makeURL(string: String) -> URL? {
|
static func makeURL(string: String) -> URL? {
|
||||||
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
try? URL.ParseStrategy().parse(string)
|
||||||
// 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 {
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
||||||
|
|
|
@ -7,6 +7,7 @@ let package = Package(
|
||||||
name: "Pachyderm",
|
name: "Pachyderm",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v16),
|
.iOS(.v16),
|
||||||
|
.macOS(.v13),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
// 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: [
|
||||||
// Dependencies declare other packages that this package depends on.
|
// Dependencies declare other packages that this package depends on.
|
||||||
.package(url: "https://github.com/karwa/swift-url.git", exact: "0.4.2"),
|
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
// 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(
|
.target(
|
||||||
name: "Pachyderm",
|
name: "Pachyderm",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "WebURL", package: "swift-url"),
|
|
||||||
.product(name: "WebURLFoundationExtras", package: "swift-url"),
|
|
||||||
],
|
],
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
.swiftLanguageMode(.v5)
|
.swiftLanguageMode(.v5)
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The base Mastodon API client.
|
The base Mastodon API client.
|
||||||
|
@ -202,8 +201,8 @@ public struct Client: Sendable {
|
||||||
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
|
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
|
||||||
let wellKnownResults = try await run(wellKnown).0
|
let wellKnownResults = try await run(wellKnown).0
|
||||||
if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
|
if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
|
||||||
let href = WebURL(url.href),
|
let href = try? URL.ParseStrategy().parse(url.href),
|
||||||
href.host == WebURL(self.baseURL)?.host {
|
href.host == self.baseURL.host() {
|
||||||
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
|
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
|
||||||
return try await run(nodeInfo).0
|
return try await run(nodeInfo).0
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Announcement: Decodable, Sendable, Hashable, Identifiable {
|
public struct Announcement: Decodable, Sendable, Hashable, Identifiable {
|
||||||
public let id: String
|
public let id: String
|
||||||
|
@ -60,7 +59,7 @@ extension Announcement {
|
||||||
public struct Account: Decodable, Sendable, Hashable {
|
public struct Account: Decodable, Sendable, Hashable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let username: String
|
public let username: String
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
public let acct: String
|
public let acct: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +67,7 @@ extension Announcement {
|
||||||
extension Announcement {
|
extension Announcement {
|
||||||
public struct Status: Decodable, Sendable, Hashable {
|
public struct Status: Decodable, Sendable, Hashable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,18 +7,17 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Card: Codable, Sendable {
|
public struct Card: Codable, Sendable {
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
public let title: String
|
public let title: String
|
||||||
public let description: String
|
public let description: String
|
||||||
public let image: WebURL?
|
@OptionalURLDecoder public var image: URL?
|
||||||
public let kind: Kind
|
public let kind: Kind
|
||||||
public let authorName: String?
|
public let authorName: String?
|
||||||
public let authorURL: WebURL?
|
@OptionalURLDecoder public var authorURL: URL?
|
||||||
public let providerName: String?
|
public let providerName: String?
|
||||||
public let providerURL: WebURL?
|
@OptionalURLDecoder public var providerURL: URL?
|
||||||
public let html: String?
|
public let html: String?
|
||||||
public let width: Int?
|
public let width: Int?
|
||||||
public let height: Int?
|
public let height: Int?
|
||||||
|
@ -27,15 +26,15 @@ public struct Card: Codable, Sendable {
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
url: WebURL,
|
url: URL,
|
||||||
title: String,
|
title: String,
|
||||||
description: String,
|
description: String,
|
||||||
image: WebURL? = nil,
|
image: URL? = nil,
|
||||||
kind: Card.Kind,
|
kind: Card.Kind,
|
||||||
authorName: String? = nil,
|
authorName: String? = nil,
|
||||||
authorURL: WebURL? = nil,
|
authorURL: URL? = nil,
|
||||||
providerName: String? = nil,
|
providerName: String? = nil,
|
||||||
providerURL: WebURL? = nil,
|
providerURL: URL? = nil,
|
||||||
html: String? = nil,
|
html: String? = nil,
|
||||||
width: Int? = nil,
|
width: Int? = nil,
|
||||||
height: Int? = nil,
|
height: Int? = nil,
|
||||||
|
@ -61,15 +60,15 @@ public struct Card: Codable, Sendable {
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
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.title = try container.decode(String.self, forKey: .title)
|
||||||
self.description = try container.decode(String.self, forKey: .description)
|
self.description = try container.decode(String.self, forKey: .description)
|
||||||
self.kind = try container.decode(Kind.self, forKey: .kind)
|
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.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.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.html = try? container.decodeIfPresent(String.self, forKey: .html)
|
||||||
self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
|
self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
|
||||||
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
|
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
|
||||||
|
|
|
@ -7,14 +7,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Emoji: Codable, Sendable {
|
public struct Emoji: Codable, Sendable {
|
||||||
public let shortcode: String
|
public let shortcode: String
|
||||||
// these shouldn't need to be WebURLs as they're not external resources,
|
@URLDecoder public var url: URL
|
||||||
// but some instances (pleroma?) has emoji urls that Foundation considers malformed so we use WebURL to be more lenient
|
@URLDecoder public var staticURL: URL
|
||||||
public let url: WebURL
|
|
||||||
public let staticURL: WebURL
|
|
||||||
public let visibleInPicker: Bool
|
public let visibleInPicker: Bool
|
||||||
public let category: String?
|
public let category: String?
|
||||||
|
|
||||||
|
@ -22,13 +19,8 @@ public struct Emoji: Codable, Sendable {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
||||||
do {
|
self._url = try container.decode(URLDecoder.self, forKey: .url)
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
self._staticURL = try container.decode(URLDecoder.self, forKey: .staticURL)
|
||||||
} 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.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
|
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
|
||||||
self.category = try container.decodeIfPresent(String.self, forKey: .category)
|
self.category = try container.decodeIfPresent(String.self, forKey: .category)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,10 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
public struct Hashtag: Codable, Sendable {
|
public struct Hashtag: Codable, Sendable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
/// Only present when returned from the trending hashtags endpoint
|
/// Only present when returned from the trending hashtags endpoint
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
/// Only present on Mastodon >= 4 and when logged in
|
/// Only present on Mastodon >= 4 and when logged in
|
||||||
|
@ -20,7 +18,7 @@ public struct Hashtag: Codable, Sendable {
|
||||||
|
|
||||||
public init(name: String, url: URL) {
|
public init(name: String, url: URL) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = WebURL(url)!
|
self.url = url
|
||||||
self.history = nil
|
self.history = nil
|
||||||
self.following = nil
|
self.following = nil
|
||||||
}
|
}
|
||||||
|
@ -29,7 +27,7 @@ public struct Hashtag: Codable, Sendable {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
|
// 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.history = try container.decodeIfPresent([History].self, forKey: .history)
|
||||||
self.following = try container.decodeIfPresent(Bool.self, forKey: .following)
|
self.following = try container.decodeIfPresent(Bool.self, forKey: .following)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,9 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Mention: Codable, Sendable {
|
public struct Mention: Codable, Sendable {
|
||||||
public let url: WebURL
|
@URLDecoder public var url: URL
|
||||||
public let username: String
|
public let username: String
|
||||||
public let acct: String
|
public let acct: String
|
||||||
/// The instance-local ID of the user being mentioned.
|
/// 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.username = try container.decode(String.self, forKey: .username)
|
||||||
self.acct = try container.decode(String.self, forKey: .acct)
|
self.acct = try container.decode(String.self, forKey: .acct)
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
do {
|
self._url = try container.decode(URLDecoder.self, forKey: .url)
|
||||||
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>")'")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(url: WebURL, username: String, acct: String, id: String) {
|
public init(url: URL, username: String, acct: String, id: String) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.username = username
|
self.username = username
|
||||||
self.acct = acct
|
self.acct = acct
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct Notification: Decodable, Sendable {
|
public struct Notification: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
|
@ -18,7 +17,7 @@ public struct Notification: Decodable, Sendable {
|
||||||
// Only present for pleroma emoji reactions
|
// Only present for pleroma emoji reactions
|
||||||
// Either an emoji or :shortcode: (for akkoma custom emoji reactions)
|
// Either an emoji or :shortcode: (for akkoma custom emoji reactions)
|
||||||
public let emoji: String?
|
public let emoji: String?
|
||||||
public let emojiURL: WebURL?
|
@OptionalURLDecoder public var emojiURL: URL?
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
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.account = try container.decode(Account.self, forKey: .account)
|
||||||
self.status = try container.decodeIfPresent(Status.self, forKey: .status)
|
self.status = try container.decodeIfPresent(Status.self, forKey: .status)
|
||||||
self.emoji = try container.decodeIfPresent(String.self, forKey: .emoji)
|
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> {
|
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
||||||
|
|
|
@ -6,14 +6,13 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct PushNotification: Decodable {
|
public struct PushNotification: Decodable {
|
||||||
public let accessToken: String
|
public let accessToken: String
|
||||||
public let preferredLocale: String
|
public let preferredLocale: String
|
||||||
public let notificationID: String
|
public let notificationID: String
|
||||||
public let notificationType: Notification.Kind
|
public let notificationType: Notification.Kind
|
||||||
public let icon: WebURL
|
@URLDecoder public var icon: URL
|
||||||
public let title: String
|
public let title: String
|
||||||
public let body: String
|
public let body: String
|
||||||
|
|
||||||
|
@ -29,7 +28,7 @@ public struct PushNotification: Decodable {
|
||||||
self.notificationID = i.description
|
self.notificationID = i.description
|
||||||
}
|
}
|
||||||
self.notificationType = try container.decode(Notification.Kind.self, forKey: .notificationType)
|
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.title = try container.decode(String.self, forKey: .title)
|
||||||
self.body = try container.decode(String.self, forKey: .body)
|
self.body = try container.decode(String.self, forKey: .body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public final class Status: StatusProtocol, Decodable, Sendable {
|
public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
/// The pseudo-visibility used by instance types (Akkoma) that overload the visibility for local-only posts.
|
/// 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 id: String
|
||||||
public let uri: 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 account: Account
|
||||||
public let inReplyToID: String?
|
public let inReplyToID: String?
|
||||||
public let inReplyToAccountID: 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.id = try container.decode(String.self, forKey: .id)
|
||||||
self.uri = try container.decode(String.self, forKey: .uri)
|
self.uri = try container.decode(String.self, forKey: .uri)
|
||||||
do {
|
do {
|
||||||
self.url = try container.decodeIfPresent(WebURL.self, forKey: .url)
|
self._url = try container.decodeIfPresent(OptionalURLDecoder.self, forKey: .url) ?? nil
|
||||||
} catch {
|
} catch {
|
||||||
let s = try? container.decode(String.self, forKey: .url)
|
let s = try? container.decode(String.self, forKey: .url)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
self.url = nil
|
self._url = OptionalURLDecoder(wrappedValue: nil)
|
||||||
} else {
|
} 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)
|
self.account = try container.decode(Account.self, forKey: .account)
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import WebURL
|
|
||||||
|
|
||||||
public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
||||||
public private(set) var notifications: [Notification]
|
public private(set) var notifications: [Notification]
|
||||||
|
@ -150,7 +149,7 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
||||||
case poll
|
case poll
|
||||||
case update
|
case update
|
||||||
case status
|
case status
|
||||||
case emojiReaction(String, WebURL?)
|
case emojiReaction(String, URL?)
|
||||||
case unknown
|
case unknown
|
||||||
|
|
||||||
var notificationKind: Notification.Kind {
|
var notificationKind: Notification.Kind {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,20 +6,21 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
import WebURL
|
@testable import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class URLTests: XCTestCase {
|
class URLTests: XCTestCase {
|
||||||
|
|
||||||
func testDecodeURL() {
|
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("https://xn--baw-joa.social/test/é"))
|
||||||
XCTAssertNotNil(try? URL.ParseStrategy().parse("http://見.香港/热狗/🌭"))
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ComposeUI
|
import ComposeUI
|
||||||
import TuskerComponents
|
import TuskerComponents
|
||||||
import WebURLFoundationExtras
|
|
||||||
import Combine
|
import Combine
|
||||||
import TuskerPreferences
|
import TuskerPreferences
|
||||||
|
|
||||||
|
@ -46,7 +45,7 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
||||||
currentAccountContainerView: { AnyView(SwitchAccountContainerView(content: $0, mastodonContextPublisher: mastodonContextPublisher)) },
|
currentAccountContainerView: { AnyView(SwitchAccountContainerView(content: $0, mastodonContextPublisher: mastodonContextPublisher)) },
|
||||||
replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") },
|
replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") },
|
||||||
emojiImageView: {
|
emojiImageView: {
|
||||||
AnyView(AsyncImage(url: URL($0.url)!) {
|
AnyView(AsyncImage(url: $0.url) {
|
||||||
$0
|
$0
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
|
|
|
@ -103,7 +103,6 @@
|
||||||
D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E02BC61C6700208903 /* UserAccounts */; };
|
D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E02BC61C6700208903 /* UserAccounts */; };
|
||||||
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E42BC6313400208903 /* Pachyderm */; };
|
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E42BC6313400208903 /* Pachyderm */; };
|
||||||
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D630C4222BC7842C00208903 /* HTMLStreamer */; };
|
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 */; };
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.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 */; };
|
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 */; };
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.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 */; };
|
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 */; };
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
||||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
||||||
|
@ -818,7 +816,6 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D630C4252BC7845800208903 /* WebURL in Frameworks */,
|
|
||||||
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
|
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
|
||||||
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
|
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
|
||||||
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
|
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
|
||||||
|
@ -855,7 +852,6 @@
|
||||||
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */,
|
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */,
|
||||||
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
||||||
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
|
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
|
||||||
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
||||||
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
|
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
|
||||||
);
|
);
|
||||||
|
@ -1781,7 +1777,6 @@
|
||||||
D630C3E02BC61C6700208903 /* UserAccounts */,
|
D630C3E02BC61C6700208903 /* UserAccounts */,
|
||||||
D630C3E42BC6313400208903 /* Pachyderm */,
|
D630C3E42BC6313400208903 /* Pachyderm */,
|
||||||
D630C4222BC7842C00208903 /* HTMLStreamer */,
|
D630C4222BC7842C00208903 /* HTMLStreamer */,
|
||||||
D630C4242BC7845800208903 /* WebURL */,
|
|
||||||
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
|
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
|
||||||
);
|
);
|
||||||
productName = NotificationExtension;
|
productName = NotificationExtension;
|
||||||
|
@ -1833,7 +1828,6 @@
|
||||||
);
|
);
|
||||||
name = Tusker;
|
name = Tusker;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
|
||||||
D674A50827F9128D00BA03AC /* Pachyderm */,
|
D674A50827F9128D00BA03AC /* Pachyderm */,
|
||||||
D6552366289870790048A653 /* ScreenCorners */,
|
D6552366289870790048A653 /* ScreenCorners */,
|
||||||
D63CC701290EC0B8000E19DE /* Sentry */,
|
D63CC701290EC0B8000E19DE /* Sentry */,
|
||||||
|
@ -1960,7 +1954,6 @@
|
||||||
);
|
);
|
||||||
mainGroup = D6D4DDC3212518A000E1C4BB;
|
mainGroup = D6D4DDC3212518A000E1C4BB;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
|
||||||
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
||||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||||
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
|
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
|
||||||
|
@ -3274,14 +3267,6 @@
|
||||||
minimumVersion = 1.0.1;
|
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 */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -3319,11 +3304,6 @@
|
||||||
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
|
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
|
||||||
productName = HTMLStreamer;
|
productName = HTMLStreamer;
|
||||||
};
|
};
|
||||||
D630C4242BC7845800208903 /* WebURL */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
|
|
||||||
productName = WebURL;
|
|
||||||
};
|
|
||||||
D635237029B78A7D009ED5E7 /* TuskerComponents */ = {
|
D635237029B78A7D009ED5E7 /* TuskerComponents */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = TuskerComponents;
|
productName = TuskerComponents;
|
||||||
|
@ -3342,11 +3322,6 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = TTTKit;
|
productName = TTTKit;
|
||||||
};
|
};
|
||||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
|
|
||||||
productName = WebURLFoundationExtras;
|
|
||||||
};
|
|
||||||
D674A50827F9128D00BA03AC /* Pachyderm */ = {
|
D674A50827F9128D00BA03AC /* Pachyderm */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Pachyderm;
|
productName = Pachyderm;
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
@objc(FollowedHashtag)
|
@objc(FollowedHashtag)
|
||||||
public final class FollowedHashtag: NSManagedObject {
|
public final class FollowedHashtag: NSManagedObject {
|
||||||
|
@ -33,6 +32,6 @@ extension FollowedHashtag {
|
||||||
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
convenience init(hashtag: Hashtag, context: NSManagedObjectContext) {
|
||||||
self.init(context: context)
|
self.init(context: context)
|
||||||
self.name = hashtag.name
|
self.name = hashtag.name
|
||||||
self.url = URL(hashtag.url)!
|
self.url = hashtag.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
|
|
||||||
@objc(SavedHashtag)
|
@objc(SavedHashtag)
|
||||||
|
@ -42,6 +41,6 @@ extension SavedHashtag {
|
||||||
self.init(context: context)
|
self.init(context: context)
|
||||||
self.accountID = account.id
|
self.accountID = account.id
|
||||||
self.name = hashtag.name
|
self.name = hashtag.name
|
||||||
self.url = URL(hashtag.url)!
|
self.url = hashtag.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
@objc(StatusMO)
|
@objc(StatusMO)
|
||||||
public final class StatusMO: NSManagedObject, StatusProtocol {
|
public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
|
@ -136,7 +135,7 @@ extension StatusMO {
|
||||||
self.sensitive = status.sensitive
|
self.sensitive = status.sensitive
|
||||||
self.spoilerText = status.spoilerText
|
self.spoilerText = status.spoilerText
|
||||||
self.uri = status.uri
|
self.uri = status.uri
|
||||||
self.url = status.url != nil ? URL(status.url!) : nil
|
self.url = status.url
|
||||||
self.visibility = status.visibility
|
self.visibility = status.visibility
|
||||||
self.poll = status.poll
|
self.poll = status.poll
|
||||||
self.localOnly = status.localOnly ?? false
|
self.localOnly = status.localOnly ?? false
|
||||||
|
|
|
@ -8,8 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
import WebURL
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class HTMLConverter {
|
class HTMLConverter {
|
||||||
|
|
||||||
|
@ -45,17 +44,7 @@ extension HTMLConverter {
|
||||||
// note: this is duplicated in NotificationExtension
|
// note: this is duplicated in NotificationExtension
|
||||||
struct Callbacks: HTMLConversionCallbacks {
|
struct Callbacks: HTMLConversionCallbacks {
|
||||||
static func makeURL(string: String) -> URL? {
|
static func makeURL(string: String) -> URL? {
|
||||||
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
try? URL.ParseStrategy().parse(string)
|
||||||
// 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 {
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURL
|
|
||||||
|
|
||||||
class AnnouncementContentTextView: ContentTextView {
|
class AnnouncementContentTextView: ContentTextView {
|
||||||
|
|
||||||
|
@ -30,7 +29,7 @@ class AnnouncementContentTextView: ContentTextView {
|
||||||
|
|
||||||
override func getMention(for url: URL, text: String) -> Mention? {
|
override func getMention(for url: URL, text: String) -> Mention? {
|
||||||
announcement?.mentions.first {
|
announcement?.mentions.first {
|
||||||
URL($0.url) == url
|
$0.url == url
|
||||||
}.map {
|
}.map {
|
||||||
Mention(url: $0.url, username: $0.username, acct: $0.acct, id: $0.id)
|
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? {
|
override func getHashtag(for url: URL, text: String) -> Hashtag? {
|
||||||
announcement?.tags.first {
|
announcement?.tags.first {
|
||||||
URL($0.url) == url
|
$0.url == url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import TuskerComponents
|
import TuskerComponents
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
struct AnnouncementListRow: View {
|
struct AnnouncementListRow: View {
|
||||||
@Binding var announcement: Announcement
|
@Binding var announcement: Announcement
|
||||||
|
@ -116,8 +115,8 @@ struct AnnouncementListRow: View {
|
||||||
let url: URL?
|
let url: URL?
|
||||||
let staticURL: URL?
|
let staticURL: URL?
|
||||||
if case .custom(let emoji) = reaction {
|
if case .custom(let emoji) = reaction {
|
||||||
url = URL(emoji.url)
|
url = emoji.url
|
||||||
staticURL = URL(emoji.staticURL)
|
staticURL = emoji.staticURL
|
||||||
} else {
|
} else {
|
||||||
url = nil
|
url = nil
|
||||||
staticURL = nil
|
staticURL = nil
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURL
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
private let mastodonRemoteStatusRegex = try! NSRegularExpression(pattern: "^/@.+@.+/\\d{18}")
|
private let mastodonRemoteStatusRegex = try! NSRegularExpression(pattern: "^/@.+@.+/\\d{18}")
|
||||||
private func isLikelyMastodonRemoteStatus(url: URL) -> Bool {
|
private func isLikelyMastodonRemoteStatus(url: URL) -> Bool {
|
||||||
|
@ -229,10 +227,10 @@ class ConversationViewController: UIViewController {
|
||||||
let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") {
|
let location = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "location") {
|
||||||
effectiveURL = location
|
effectiveURL = location
|
||||||
} else {
|
} else {
|
||||||
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
|
effectiveURL = url.formatted(.url.fragment(.never))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
effectiveURL = WebURL(url)!.serialized(excludingFragment: true)
|
effectiveURL = url.formatted(.url.fragment(.never))
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)
|
let request = Client.search(query: effectiveURL, types: [.statuses], resolve: true)
|
||||||
|
|
|
@ -10,7 +10,6 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import CoreData
|
import CoreData
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
|
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
|
||||||
|
|
||||||
|
@ -560,10 +559,7 @@ extension ExploreViewController: UICollectionViewDragDelegate {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider = NSItemProvider(object: activity)
|
provider = NSItemProvider(object: activity)
|
||||||
case let .savedHashtag(hashtag):
|
case let .savedHashtag(hashtag):
|
||||||
guard let url = URL(hashtag.url) else {
|
provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
return []
|
|
||||||
}
|
|
||||||
provider = NSItemProvider(object: url as NSURL)
|
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class TrendingHashtagsViewController: UIViewController, CollectionViewController {
|
class TrendingHashtagsViewController: UIViewController, CollectionViewController {
|
||||||
|
@ -277,11 +276,10 @@ extension TrendingHashtagsViewController: UICollectionViewDelegate {
|
||||||
extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
|
extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
case let .tag(hashtag) = item,
|
case let .tag(hashtag) = item else {
|
||||||
let url = URL(hashtag.url) else {
|
|
||||||
return []
|
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) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
|
|
||||||
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
|
@ -71,7 +70,7 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
self.card = card
|
self.card = card
|
||||||
self.thumbnailView.image = nil
|
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)
|
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
titleLabel.text = title
|
titleLabel.text = title
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
|
|
||||||
struct TrendingLinkCardView: View {
|
struct TrendingLinkCardView: View {
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Combine
|
import Combine
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
|
@ -293,21 +292,19 @@ extension TrendingLinksViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
|
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
let url = URL(card.url) else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selected(url: url)
|
selected(url: card.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
|
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
|
||||||
let url = URL(card.url),
|
|
||||||
let cell = collectionView.cellForItem(at: indexPath) else {
|
let cell = collectionView.cellForItem(at: indexPath) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
let vc = SFSafariViewController(url: url)
|
let vc = SFSafariViewController(url: card.url)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
||||||
#endif
|
#endif
|
||||||
|
@ -324,11 +321,10 @@ extension TrendingLinksViewController: UICollectionViewDelegate {
|
||||||
|
|
||||||
extension TrendingLinksViewController: UICollectionViewDragDelegate {
|
extension TrendingLinksViewController: UICollectionViewDragDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath),
|
guard case .link(let card) = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
let url = URL(card.url) else {
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
|
return [UIDragItem(itemProvider: NSItemProvider(object: card.url as NSURL))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -513,9 +513,7 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||||
|
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
if let url = URL(card.url) {
|
selected(url: card.url)
|
||||||
selected(url: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
case let .status(id, state):
|
case let .status(id, state):
|
||||||
selected(status: id, state: state.copy())
|
selected(status: id, state: state.copy())
|
||||||
|
@ -544,12 +542,9 @@ extension TrendsViewController: UICollectionViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
guard let url = URL(card.url) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let cell = collectionView.cellForItem(at: indexPath)!
|
let cell = collectionView.cellForItem(at: indexPath)!
|
||||||
return UIContextMenuConfiguration {
|
return UIContextMenuConfiguration {
|
||||||
let vc = SFSafariViewController(url: url)
|
let vc = SFSafariViewController(url: card.url)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
||||||
#endif
|
#endif
|
||||||
|
@ -624,10 +619,7 @@ extension TrendsViewController: UICollectionViewDragDelegate {
|
||||||
return []
|
return []
|
||||||
|
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
guard let url = URL(hashtag.url) else {
|
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
return []
|
|
||||||
}
|
|
||||||
let provider = NSItemProvider(object: url as NSURL)
|
|
||||||
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
@ -635,10 +627,7 @@ extension TrendsViewController: UICollectionViewDragDelegate {
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
|
|
||||||
case let .link(card):
|
case let .link(card):
|
||||||
guard let url = URL(card.url) else {
|
return [UIDragItem(itemProvider: NSItemProvider(object: card.url as NSURL))]
|
||||||
return []
|
|
||||||
}
|
|
||||||
return [UIDragItem(itemProvider: NSItemProvider(object: url as NSURL))]
|
|
||||||
|
|
||||||
case let .status(id, _):
|
case let .status(id, _):
|
||||||
guard let status = mastodonController.persistentContainer.status(for: id),
|
guard let status = mastodonController.persistentContainer.status(for: id),
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
import WebURL
|
|
||||||
|
|
||||||
class FastSwitchingAccountView: UIView {
|
class FastSwitchingAccountView: UIView {
|
||||||
|
|
||||||
|
@ -131,11 +130,7 @@ class FastSwitchingAccountView: UIView {
|
||||||
|
|
||||||
private func setupAccount(account: UserAccountInfo) {
|
private func setupAccount(account: UserAccountInfo) {
|
||||||
usernameLabel.text = account.username
|
usernameLabel.text = account.username
|
||||||
if let domain = WebURL.Domain(account.instanceURL.host!) {
|
instanceLabel.text = account.instanceURL.host(percentEncoded: false)
|
||||||
instanceLabel.text = domain.render(.uncheckedUnicodeString)
|
|
||||||
} else {
|
|
||||||
instanceLabel.text = account.instanceURL.host!
|
|
||||||
}
|
|
||||||
let controller = MastodonController.getForAccount(account)
|
let controller = MastodonController.getForAccount(account)
|
||||||
avatarTask = Task {
|
avatarTask = Task {
|
||||||
guard let account = try? await controller.getOwnAccount(),
|
guard let account = try? await controller.getOwnAccount(),
|
||||||
|
|
|
@ -155,7 +155,7 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||||
fetchCustomEmojiImage?.1.cancel()
|
fetchCustomEmojiImage?.1.cancel()
|
||||||
case .emojiReaction(let emojiOrShortcode, let url):
|
case .emojiReaction(let emojiOrShortcode, let url):
|
||||||
iconImageView.image = nil
|
iconImageView.image = nil
|
||||||
if let url = url.flatMap({ URL($0) }),
|
if let url,
|
||||||
fetchCustomEmojiImage?.0 != url {
|
fetchCustomEmojiImage?.0 != url {
|
||||||
fetchCustomEmojiImage?.1.cancel()
|
fetchCustomEmojiImage?.1.cancel()
|
||||||
let task = Task {
|
let task = Task {
|
||||||
|
|
|
@ -740,7 +740,7 @@ extension NotificationsCollectionViewController: UICollectionViewDragDelegate {
|
||||||
return cell.dragItemsForBeginning(session: session)
|
return cell.dragItemsForBeginning(session: session)
|
||||||
case .poll, .update:
|
case .poll, .update:
|
||||||
let status = group.notifications.first!.status!
|
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)
|
let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: mastodonController.accountInfo!.id)
|
||||||
activity.displaysAuxiliaryScene = true
|
activity.displaysAuxiliaryScene = true
|
||||||
provider.registerObject(activity, visibility: .all)
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURL
|
|
||||||
|
|
||||||
struct MockStatusView: View {
|
struct MockStatusView: View {
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
@ObservedObject private var preferences = Preferences.shared
|
||||||
|
@ -136,8 +135,8 @@ private struct MockStatusCardView: UIViewRepresentable {
|
||||||
let view = StatusCardView()
|
let view = StatusCardView()
|
||||||
view.isUserInteractionEnabled = false
|
view.isUserInteractionEnabled = false
|
||||||
let card = StatusCardView.CardData(
|
let card = StatusCardView.CardData(
|
||||||
url: WebURL("https://vaccor.space/tusker")!,
|
url: URL(string: "https://vaccor.space/tusker")!,
|
||||||
image: WebURL("https://vaccor.space/tusker/img/icon.png")!,
|
image: URL(string: "https://vaccor.space/tusker/img/icon.png")!,
|
||||||
title: "Tusker",
|
title: "Tusker",
|
||||||
description: "Tusker is an iOS app for Mastodon"
|
description: "Tusker is an iOS app for Mastodon"
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
import WebURL
|
|
||||||
|
|
||||||
struct PrefsAccountView: View {
|
struct PrefsAccountView: View {
|
||||||
let account: UserAccountInfo
|
let account: UserAccountInfo
|
||||||
|
@ -19,12 +18,7 @@ struct PrefsAccountView: View {
|
||||||
VStack(alignment: .prefsAvatar) {
|
VStack(alignment: .prefsAvatar) {
|
||||||
Text(verbatim: account.username)
|
Text(verbatim: account.username)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
let instance = if let domain = WebURL.Domain(account.instanceURL.host!) {
|
Text(verbatim: account.instanceURL.host(percentEncoded: false)!)
|
||||||
domain.render(.uncheckedUnicodeString)
|
|
||||||
} else {
|
|
||||||
account.instanceURL.host!
|
|
||||||
}
|
|
||||||
Text(verbatim: instance)
|
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
fileprivate let accountCell = "accountCell"
|
fileprivate let accountCell = "accountCell"
|
||||||
fileprivate let statusCell = "statusCell"
|
fileprivate let statusCell = "statusCell"
|
||||||
|
@ -538,7 +537,7 @@ extension SearchResultsViewController: UICollectionViewDragDelegate {
|
||||||
url = account.url
|
url = account.url
|
||||||
activity = UserActivityManager.showProfileActivity(id: id, accountID: accountInfo.id)
|
activity = UserActivityManager.showProfileActivity(id: id, accountID: accountInfo.id)
|
||||||
case .hashtag(let tag):
|
case .hashtag(let tag):
|
||||||
url = URL(tag.url)!
|
url = tag.url
|
||||||
activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: accountInfo.id)!
|
activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: accountInfo.id)!
|
||||||
case .status(let id, _):
|
case .status(let id, _):
|
||||||
guard let status = mastodonController.persistentContainer.status(for: id),
|
guard let status = mastodonController.persistentContainer.status(for: id),
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURL
|
|
||||||
|
|
||||||
class StatusActionAccountListViewController: UIViewController {
|
class StatusActionAccountListViewController: UIViewController {
|
||||||
|
|
||||||
|
@ -183,7 +182,7 @@ extension StatusActionAccountListViewController {
|
||||||
enum ActionType {
|
enum ActionType {
|
||||||
case favorite
|
case favorite
|
||||||
case reblog
|
case reblog
|
||||||
case emojiReaction(String, WebURL?)
|
case emojiReaction(String, URL?)
|
||||||
|
|
||||||
init?(_ groupKind: NotificationGroup.Kind) {
|
init?(_ groupKind: NotificationGroup.Kind) {
|
||||||
switch groupKind {
|
switch groupKind {
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -154,12 +153,7 @@ extension MenuActionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let shareSection: [UIMenuElement]
|
let shareSection = actionsForURL(hashtag.url, source: source)
|
||||||
if let url = URL(hashtag.url) {
|
|
||||||
shareSection = actionsForURL(url, source: source)
|
|
||||||
} else {
|
|
||||||
shareSection = []
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||||
|
@ -375,14 +369,11 @@ extension MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {
|
func actionsForTrendingLink(card: Card, source: PopoverSource) -> [UIMenuElement] {
|
||||||
guard let url = URL(card.url) else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: url),
|
openInSafariAction(url: card.url),
|
||||||
createAction(identifier: "share", title: "Share…", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
createAction(identifier: "share", title: "Share…", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
||||||
guard let self else { return }
|
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
|
createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
@ -393,7 +384,7 @@ extension MenuActionProvider {
|
||||||
text += title
|
text += title
|
||||||
text += ":\n"
|
text += ":\n"
|
||||||
}
|
}
|
||||||
text += url.absoluteString
|
text += card.url.absoluteString
|
||||||
|
|
||||||
let draft = self.mastodonController!.createDraft(text: text)
|
let draft = self.mastodonController!.createDraft(text: text)
|
||||||
self.navigationDelegate?.compose(editing: draft)
|
self.navigationDelegate?.compose(editing: draft)
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
@ -54,7 +53,7 @@ struct AccountDisplayNameView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
group.enter()
|
group.enter()
|
||||||
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
|
let request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||||
defer { group.leave() }
|
defer { group.leave() }
|
||||||
guard let image = image else { return }
|
guard let image = image else { return }
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
@ -73,7 +72,7 @@ extension BaseEmojiLabel {
|
||||||
|
|
||||||
foundEmojis = true
|
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.
|
// if the image is cached, add it immediately.
|
||||||
// we generate the thumbnail on the main thread, because it's usually fast enough
|
// we generate the thumbnail on the main thread, because it's usually fast enough
|
||||||
// and the delay caused by doing it asynchronously looks works.
|
// and the delay caused by doing it asynchronously looks works.
|
||||||
|
@ -90,7 +89,7 @@ extension BaseEmojiLabel {
|
||||||
// otherwise, perform the network request
|
// otherwise, perform the network request
|
||||||
|
|
||||||
group.enter()
|
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 {
|
guard let image else {
|
||||||
group.leave()
|
group.leave()
|
||||||
return
|
return
|
||||||
|
@ -98,7 +97,7 @@ extension BaseEmojiLabel {
|
||||||
image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in
|
image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in
|
||||||
guard let thumbnail = thumbnail?.cgImage,
|
guard let thumbnail = thumbnail?.cgImage,
|
||||||
case let rescaled = UIImage(cgImage: thumbnail, scale: screenScale, orientation: .up),
|
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()
|
group.leave()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import WebURL
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
struct CustomEmojiImageView: View {
|
struct CustomEmojiImageView: View {
|
||||||
let emoji: Emoji
|
let emoji: Emoji
|
||||||
|
@ -35,7 +34,7 @@ struct CustomEmojiImageView: View {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private func loadImage() {
|
private func loadImage() {
|
||||||
request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in
|
request = ImageCache.emojis.get(emoji.url) { (_, image) in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.request = nil
|
self.request = nil
|
||||||
if let image = image {
|
if let image = image {
|
||||||
|
|
|
@ -9,8 +9,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import WebURL
|
|
||||||
import WebURLFoundationExtras
|
|
||||||
import HTMLStreamer
|
import HTMLStreamer
|
||||||
|
|
||||||
class StatusCardView: UIView {
|
class StatusCardView: UIView {
|
||||||
|
@ -184,14 +182,14 @@ class StatusCardView: UIView {
|
||||||
if sensitive {
|
if sensitive {
|
||||||
if let blurhash = card.blurhash {
|
if let blurhash = card.blurhash {
|
||||||
imageView.blurImage = false
|
imageView.blurImage = false
|
||||||
imageView.showOnlyBlurHash(blurhash, for: URL(image)!)
|
imageView.showOnlyBlurHash(blurhash, for: image)
|
||||||
} else {
|
} else {
|
||||||
// if we don't have a blurhash, load the image and show it behind a blur
|
// if we don't have a blurhash, load the image and show it behind a blur
|
||||||
imageView.blurImage = true
|
imageView.blurImage = true
|
||||||
imageView.update(for: URL(image), blurhash: nil)
|
imageView.update(for: image, blurhash: nil)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
imageView.update(for: URL(image), blurhash: card.blurhash)
|
imageView.update(for: image, blurhash: card.blurhash)
|
||||||
}
|
}
|
||||||
imageView.isHidden = false
|
imageView.isHidden = false
|
||||||
leadingSpacer.isHidden = true
|
leadingSpacer.isHidden = true
|
||||||
|
@ -210,8 +208,8 @@ class StatusCardView: UIView {
|
||||||
descriptionLabel.text = description
|
descriptionLabel.text = description
|
||||||
descriptionLabel.isHidden = description.isEmpty
|
descriptionLabel.isHidden = description.isEmpty
|
||||||
|
|
||||||
if let host = card.url.host {
|
if let host = card.url.host(percentEncoded: false) {
|
||||||
domainLabel.text = host.serialized
|
domainLabel.text = host
|
||||||
domainLabel.isHidden = false
|
domainLabel.isHidden = false
|
||||||
} else {
|
} else {
|
||||||
domainLabel.isHidden = true
|
domainLabel.isHidden = true
|
||||||
|
@ -238,7 +236,7 @@ class StatusCardView: UIView {
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
|
|
||||||
if let card = card, let delegate = navigationDelegate {
|
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 {
|
struct CardData: Equatable {
|
||||||
let url: WebURL
|
let url: URL
|
||||||
let image: WebURL?
|
let image: URL?
|
||||||
let title: String
|
let title: String
|
||||||
let description: String
|
let description: String
|
||||||
let blurhash: String?
|
let blurhash: String?
|
||||||
|
@ -262,7 +260,7 @@ class StatusCardView: UIView {
|
||||||
self.blurhash = card.blurhash
|
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.url = url
|
||||||
self.image = image
|
self.image = image
|
||||||
self.title = title
|
self.title = title
|
||||||
|
@ -278,13 +276,13 @@ extension StatusCardView: UIContextMenuInteractionDelegate {
|
||||||
guard let card = card else { return nil }
|
guard let card = card else { return nil }
|
||||||
|
|
||||||
return UIContextMenuConfiguration(identifier: nil) {
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
let vc = SFSafariViewController(url: URL(card.url)!)
|
let vc = SFSafariViewController(url: card.url)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
||||||
#endif
|
#endif
|
||||||
return vc
|
return vc
|
||||||
} actionProvider: { (_) in
|
} 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)
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
|
||||||
|
|
||||||
class StatusContentTextView: ContentTextView {
|
class StatusContentTextView: ContentTextView {
|
||||||
|
|
||||||
|
@ -26,7 +25,7 @@ class StatusContentTextView: ContentTextView {
|
||||||
let mastodonController = mastodonController,
|
let mastodonController = mastodonController,
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) {
|
let status = mastodonController.persistentContainer.status(for: statusID) {
|
||||||
mention = status.mentions.first { (mention) in
|
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.username // Mastodon and Pleroma include @ in the text
|
||||||
|| text.dropFirst() == mention.acct // Misskey includes @ and uses the whole acct
|
|| 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
|
|| 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 mastodonController = mastodonController,
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) {
|
let status = mastodonController.persistentContainer.status(for: statusID) {
|
||||||
hashtag = status.hashtags.first { (hashtag) in
|
hashtag = status.hashtags.first { (hashtag) in
|
||||||
URL(hashtag.url) == url
|
hashtag.url == url
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hashtag = nil
|
hashtag = nil
|
||||||
|
|
Loading…
Reference in New Issue