Compare commits

...

4 Commits

72 changed files with 243 additions and 162 deletions

View File

@ -484,7 +484,7 @@ public class Client {
} }
extension Client { extension Client {
public struct Error: LocalizedError { public struct Error: LocalizedError, Sendable {
public let requestMethod: Method public let requestMethod: Method
public let requestEndpoint: Endpoint public let requestEndpoint: Endpoint
public let type: ErrorType public let type: ErrorType
@ -519,7 +519,7 @@ extension Client {
} }
} }
} }
public enum ErrorType: LocalizedError { public enum ErrorType: LocalizedError, Sendable {
case networkError(Swift.Error) case networkError(Swift.Error)
case unexpectedStatus(Int) case unexpectedStatus(Int)
case invalidRequest case invalidRequest

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public final class Account: AccountProtocol, Decodable { public final class Account: AccountProtocol, Decodable, Sendable {
public let id: String public let id: String
public let username: String public let username: String
public let acct: String public let acct: String
@ -25,7 +25,7 @@ public final class Account: AccountProtocol, Decodable {
public let avatarStatic: URL? public let avatarStatic: URL?
public let header: URL? public let header: URL?
public let headerStatic: URL? public let headerStatic: URL?
public private(set) var emojis: [Emoji] public let emojis: [Emoji]
public let moved: Bool? public let moved: Bool?
public let movedTo: Account? public let movedTo: Account?
public let fields: [Field] public let fields: [Field]
@ -171,7 +171,7 @@ extension Account: CustomDebugStringConvertible {
} }
extension Account { extension Account {
public struct Field: Codable, Equatable { public struct Field: Codable, Equatable, Sendable {
public let name: String public let name: String
public let value: String public let value: String
public let verifiedAt: Date? public let verifiedAt: Date?

View File

@ -8,11 +8,11 @@
import Foundation import Foundation
public class Application: Decodable { public struct Application: Decodable, Sendable {
public let name: String public let name: String
public let website: URL? public let website: URL?
public required 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.name = try container.decode(String.self, forKey: .name) self.name = try container.decode(String.self, forKey: .name)

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class Attachment: Codable { public struct Attachment: Codable, Sendable {
public let id: String public let id: String
public let kind: Kind public let kind: Kind
public let url: URL public let url: URL
@ -25,7 +25,7 @@ public class Attachment: Codable {
], nil)) ], nil))
} }
required 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.id = try container.decode(String.self, forKey: .id) self.id = try container.decode(String.self, forKey: .id)
self.kind = try container.decode(Kind.self, forKey: .kind) self.kind = try container.decode(Kind.self, forKey: .kind)
@ -50,7 +50,7 @@ public class Attachment: Codable {
} }
extension Attachment { extension Attachment {
public enum Kind: String, Codable { public enum Kind: String, Codable, Sendable {
case image case image
case video case video
case gifv case gifv
@ -77,7 +77,7 @@ extension Attachment {
} }
extension Attachment { extension Attachment {
public struct Metadata: Codable { public struct Metadata: Codable, Sendable {
public let length: String? public let length: String?
public let duration: Float? public let duration: Float?
public let audioEncoding: String? public let audioEncoding: String?
@ -108,7 +108,7 @@ extension Attachment {
} }
} }
public struct ImageMetadata: Codable { public struct ImageMetadata: Codable, Sendable {
public let width: Int? public let width: Int?
public let height: Int? public let height: Int?
public let size: String? public let size: String?

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import WebURL import WebURL
public class Card: Codable { public struct Card: Codable, Sendable {
public let url: WebURL public let url: WebURL
public let title: String public let title: String
public let description: String public let description: String
@ -26,7 +26,7 @@ public class Card: Codable {
/// Only present when returned from the trending links endpoint /// Only present when returned from the trending links endpoint
public let history: [History]? public let history: [History]?
public required 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(WebURL.self, forKey: .url)
@ -75,7 +75,7 @@ public class Card: Codable {
} }
extension Card { extension Card {
public enum Kind: String, Codable { public enum Kind: String, Codable, Sendable {
case link case link
case photo case photo
case video case video

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class ConversationContext: Decodable { public struct ConversationContext: Decodable, Sendable {
public let ancestors: [Status] public let ancestors: [Status]
public let descendants: [Status] public let descendants: [Status]

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum DirectoryOrder: String, CaseIterable { public enum DirectoryOrder: String, CaseIterable, Sendable {
case active case active
case new case new
} }

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import WebURL import WebURL
public class Emoji: Codable { 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, // 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 // but some instances (pleroma?) has emoji urls that Foundation considers malformed so we use WebURL to be more lenient
@ -18,7 +18,7 @@ public class Emoji: Codable {
public let visibleInPicker: Bool public let visibleInPicker: Bool
public let category: String? public let category: String?
public required 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.shortcode = try container.decode(String.self, forKey: .shortcode) self.shortcode = try container.decode(String.self, forKey: .shortcode)

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct FilterV1: Decodable { public struct FilterV1: Decodable, Sendable {
public let id: String public let id: String
public let phrase: String public let phrase: String
private let context: [String] private let context: [String]
@ -45,7 +45,7 @@ public struct FilterV1: Decodable {
} }
extension FilterV1 { extension FilterV1 {
public enum Context: String, Decodable, CaseIterable { public enum Context: String, Decodable, CaseIterable, Sendable {
case home case home
case notifications case notifications
case `public` case `public`

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public struct FilterV2: Decodable { public struct FilterV2: Decodable, Sendable {
public let id: String public let id: String
public let title: String public let title: String
public let context: [FilterV1.Context] public let context: [FilterV1.Context]
@ -80,14 +80,14 @@ public struct FilterV2: Decodable {
} }
extension FilterV2 { extension FilterV2 {
public enum Action: String, Decodable, Hashable, CaseIterable { public enum Action: String, Decodable, Hashable, CaseIterable, Sendable {
case warn case warn
case hide case hide
} }
} }
extension FilterV2 { extension FilterV2 {
public struct Keyword: Decodable { public struct Keyword: Decodable, Sendable {
public let id: String public let id: String
public let keyword: String public let keyword: String
public let wholeWord: Bool public let wholeWord: Bool

View File

@ -10,7 +10,7 @@ import Foundation
import WebURL import WebURL
import WebURLFoundationExtras import WebURLFoundationExtras
public class Hashtag: Codable { public struct Hashtag: Codable, Sendable {
public let name: String public let name: String
public let url: WebURL public let url: WebURL
/// Only present when returned from the trending hashtags endpoint /// Only present when returned from the trending hashtags endpoint
@ -25,7 +25,7 @@ public class Hashtag: Codable {
self.following = nil self.following = nil
} }
public required 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.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

View File

@ -8,12 +8,12 @@
import Foundation import Foundation
public class History: Codable { public struct History: Codable, Sendable {
public let day: Date public let day: Date
public let uses: Int public let uses: Int
public let accounts: Int public let accounts: Int
public required 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)
if let day = try? container.decode(Date.self, forKey: .day) { if let day = try? container.decode(Date.self, forKey: .day) {

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class Instance: Decodable { public struct Instance: Decodable, Sendable {
public let uri: String public let uri: String
public let title: String public let title: String
public let description: String public let description: String
@ -37,7 +37,7 @@ public class Instance: Decodable {
} }
// we need a custom decoder, because all API-compatible implementations don't return some data in the same format // we need a custom decoder, because all API-compatible implementations don't return some data in the same format
public required 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.uri = try container.decode(String.self, forKey: .uri) self.uri = try container.decode(String.self, forKey: .uri)
self.title = try container.decode(String.self, forKey: .title) self.title = try container.decode(String.self, forKey: .title)
@ -93,7 +93,7 @@ public class Instance: Decodable {
} }
extension Instance { extension Instance {
public struct Stats: Decodable { public struct Stats: Decodable, Sendable {
public let domainCount: Int? public let domainCount: Int?
public let statusCount: Int? public let statusCount: Int?
public let userCount: Int? public let userCount: Int?
@ -107,7 +107,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct Configuration: Decodable { public struct Configuration: Decodable, Sendable {
public let statuses: StatusesConfiguration public let statuses: StatusesConfiguration
public let mediaAttachments: MediaAttachmentsConfiguration public let mediaAttachments: MediaAttachmentsConfiguration
/// Use Instance.pollsConfiguration to support older instance that don't have this nested /// Use Instance.pollsConfiguration to support older instance that don't have this nested
@ -122,7 +122,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct StatusesConfiguration: Decodable { public struct StatusesConfiguration: Decodable, Sendable {
public let maxCharacters: Int public let maxCharacters: Int
public let maxMediaAttachments: Int public let maxMediaAttachments: Int
public let charactersReservedPerURL: Int public let charactersReservedPerURL: Int
@ -136,7 +136,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct MediaAttachmentsConfiguration: Decodable { public struct MediaAttachmentsConfiguration: Decodable, Sendable {
public let supportedMIMETypes: [String] public let supportedMIMETypes: [String]
public let imageSizeLimit: Int public let imageSizeLimit: Int
public let imageMatrixLimit: Int public let imageMatrixLimit: Int
@ -156,7 +156,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct PollsConfiguration: Decodable { public struct PollsConfiguration: Decodable, Sendable {
public let maxOptions: Int public let maxOptions: Int
public let maxCharactersPerOption: Int public let maxCharactersPerOption: Int
public let minExpiration: TimeInterval public let minExpiration: TimeInterval
@ -172,7 +172,7 @@ extension Instance {
} }
extension Instance { extension Instance {
public struct Rule: Decodable, Identifiable { public struct Rule: Decodable, Identifiable, Sendable {
public let id: String public let id: String
public let text: String public let text: String
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class List: Decodable, Equatable, Hashable { public struct List: Decodable, Equatable, Hashable, Sendable {
public let id: String public let id: String
public let title: String public let title: String

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class LoginSettings: Decodable { public struct LoginSettings: Decodable, Sendable {
public let accessToken: String public let accessToken: String
private let scope: String? private let scope: String?

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import WebURL import WebURL
public struct Mention: Codable { public struct Mention: Codable, Sendable {
public let url: WebURL public let url: WebURL
public let username: String public let username: String
public let acct: String public let acct: String

View File

@ -8,11 +8,11 @@
import Foundation import Foundation
public struct NodeInfo: Decodable { public struct NodeInfo: Decodable, Sendable {
public let version: String public let version: String
public let software: Software public let software: Software
public struct Software: Decodable { public struct Software: Decodable, Sendable {
public let name: String public let name: String
public let version: String public let version: String
} }

View File

@ -8,14 +8,14 @@
import Foundation import Foundation
public class Notification: Decodable { public struct Notification: Decodable, Sendable {
public let id: String public let id: String
public let kind: Kind public let kind: Kind
public let createdAt: Date public let createdAt: Date
public let account: Account public let account: Account
public let status: Status? public let status: Status?
public required 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.id = try container.decode(String.self, forKey: .id) self.id = try container.decode(String.self, forKey: .id)
@ -45,7 +45,7 @@ public class Notification: Decodable {
} }
extension Notification { extension Notification {
public enum Kind: String, Decodable, CaseIterable { public enum Kind: String, Decodable, CaseIterable, Sendable {
case mention case mention
case reblog case reblog
case favourite case favourite

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public final class Poll: Codable { public struct Poll: Codable, Sendable {
public let id: String public let id: String
public let expiresAt: Date? public let expiresAt: Date?
public let expired: Bool public let expired: Bool
@ -43,7 +43,7 @@ public final class Poll: Codable {
} }
extension Poll { extension Poll {
public final class Option: Codable { public struct Option: Codable, Sendable {
public let title: String public let title: String
public let votesCount: Int? public let votesCount: Int?

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class PushSubscription: Decodable { public struct PushSubscription: Decodable, Sendable {
public let id: String public let id: String
public let endpoint: URL public let endpoint: URL
public let serverKey: String public let serverKey: String

View File

@ -8,12 +8,12 @@
import Foundation import Foundation
public class RegisteredApplication: Decodable { public struct RegisteredApplication: Decodable, Sendable {
public let id: String public let id: String
public let clientID: String public let clientID: String
public let clientSecret: String public let clientSecret: String
public required 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)
// Pixelfed API returns id/client_id as numbers instead of strings // Pixelfed API returns id/client_id as numbers instead of strings

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class Relationship: Decodable { public struct Relationship: Decodable, Sendable {
public let id: String public let id: String
public let following: Bool public let following: Bool
public let followedBy: Bool public let followedBy: Bool

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class Report: Decodable { public struct Report: Decodable, Sendable {
public let id: String public let id: String
public let actionTaken: Bool public let actionTaken: Bool

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum Scope: String { public enum Scope: String, Sendable {
case read case read
case write case write
case follow case follow

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum SearchResultType: String { public enum SearchResultType: String, Sendable {
case accounts case accounts
case hashtags case hashtags
case statuses case statuses

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public class SearchResults: Decodable { public struct SearchResults: Decodable, Sendable {
public let accounts: [Account] public let accounts: [Account]
public let statuses: [Status] public let statuses: [Status]
public let hashtags: [Hashtag] public let hashtags: [Hashtag]

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import WebURL import WebURL
public final class Status: StatusProtocol, Decodable { 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? public let url: WebURL?
@ -188,7 +188,7 @@ public final class Status: StatusProtocol, Decodable {
} }
extension Status { extension Status {
public enum Visibility: String, Codable, CaseIterable { public enum Visibility: String, Codable, CaseIterable, Sendable {
case `public` case `public`
case unlisted case unlisted
case `private` case `private`

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum StatusContentType: String, Codable, CaseIterable { public enum StatusContentType: String, Codable, CaseIterable, Sendable {
case plain, markdown, html case plain, markdown, html
var mimeType: String { var mimeType: String {

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public struct Suggestion: Decodable { public struct Suggestion: Decodable, Sendable {
public let source: Source public let source: Source
public let account: Account public let account: Account
@ -17,7 +17,7 @@ public struct Suggestion: Decodable {
} }
extension Suggestion { extension Suggestion {
public enum Source: String, Decodable { public enum Source: String, Decodable, Sendable {
case staff case staff
case pastInteractions = "past_interactions" case pastInteractions = "past_interactions"
case global case global

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum Timeline: Equatable, Hashable { public enum Timeline: Equatable, Hashable, Sendable {
case home case home
case `public`(local: Bool) case `public`(local: Bool)
case tag(hashtag: String) case tag(hashtag: String)

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public struct TimelineMarkers: Decodable { public struct TimelineMarkers: Decodable, Sendable {
public let home: Marker? public let home: Marker?
public let notifications: Marker? public let notifications: Marker?
@ -26,7 +26,7 @@ public struct TimelineMarkers: Decodable {
case notifications case notifications
} }
public struct Marker: Decodable { public struct Marker: Decodable, Sendable {
public let lastReadID: String public let lastReadID: String
public let version: Int public let version: Int
public let updatedAt: Date public let updatedAt: Date

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
struct WellKnown: Decodable { struct WellKnown: Decodable, Sendable {
let links: [Link] let links: [Link]
struct Link: Decodable { struct Link: Decodable {

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
protocol Body { protocol Body: Sendable {
var mimeType: String? { get } var mimeType: String? { get }
var data: Data? { get } var data: Data? { get }
} }
@ -76,7 +76,7 @@ struct FormDataBody: Body {
} }
} }
struct JsonBody<T: Encodable>: Body { struct JsonBody<T: Encodable & Sendable>: Body {
let value: T let value: T
init(_ value: T) { init(_ value: T) {

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertible { public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertible, Sendable {
let components: [Component] let components: [Component]
public init(stringLiteral value: StringLiteralType) { public init(stringLiteral value: StringLiteralType) {
@ -54,7 +54,7 @@ public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertibl
} }
} }
enum Component { enum Component: Sendable {
case literal(String) case literal(String)
case interpolated(String) case interpolated(String)
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct FormAttachment { public struct FormAttachment: Sendable {
let mimeType: String let mimeType: String
let data: Data let data: Data
let fileName: String let fileName: String

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum Method { public enum Method: Sendable {
case get, post, put, patch, delete case get, post, put, patch, delete
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
struct Parameter { struct Parameter: Sendable {
let name: String let name: String
let value: String? let value: String?
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Request<ResultType: Decodable> { public struct Request<ResultType: Decodable>: Sendable {
let method: Method let method: Method
let endpoint: Endpoint let endpoint: Endpoint
let body: Body let body: Body

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum RequestRange { public enum RequestRange: Sendable {
case `default` case `default`
case count(Int) case count(Int)
/// Chronologically immediately before the given ID /// Chronologically immediately before the given ID

View File

@ -8,6 +8,6 @@
import Foundation import Foundation
public struct Empty: Decodable { public struct Empty: Decodable, Sendable {
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct Pagination { public struct Pagination: Sendable {
public let older: RequestRange? public let older: RequestRange?
public let newer: RequestRange? public let newer: RequestRange?
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public enum Response<Result: Decodable> { public enum Response<Result: Decodable & Sendable>: Sendable {
case success(Result, Pagination?) case success(Result, Pagination?)
case failure(Client.Error) case failure(Client.Error)
} }

View File

@ -8,7 +8,8 @@
import Foundation import Foundation
public class CollapseState: Equatable { @MainActor
public final class CollapseState: Sendable {
public var collapsible: Bool? public var collapsible: Bool?
public var collapsed: Bool? public var collapsed: Bool?
@ -33,8 +34,4 @@ public class CollapseState: Equatable {
public static var unknown: CollapseState { public static var unknown: CollapseState {
CollapseState(collapsible: nil, collapsed: nil) CollapseState(collapsible: nil, collapsed: nil)
} }
public static func == (lhs: CollapseState, rhs: CollapseState) -> Bool {
lhs.collapsible == rhs.collapsible && lhs.collapsed == rhs.collapsed
}
} }

View File

@ -14,6 +14,7 @@ public struct NotificationGroup: Identifiable, Hashable {
public let kind: Notification.Kind public let kind: Notification.Kind
public let statusState: CollapseState? public let statusState: CollapseState?
@MainActor
init?(notifications: [Notification]) { init?(notifications: [Notification]) {
guard !notifications.isEmpty else { return nil } guard !notifications.isEmpty else { return nil }
self.notifications = notifications self.notifications = notifications
@ -51,6 +52,7 @@ public struct NotificationGroup: Identifiable, Hashable {
notifications.append(contentsOf: group.notifications) notifications.append(contentsOf: group.notifications)
} }
@MainActor
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] { public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
var groups = [NotificationGroup]() var groups = [NotificationGroup]()
for notification in notifications { for notification in notifications {

View File

@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import Combine
public class GameController: ObservableObject { public class GameController: ObservableObject {

View File

@ -220,6 +220,7 @@
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; }; D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */; };
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; }; D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; }; D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; };
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */; };
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; }; D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; };
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; }; D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
@ -252,7 +253,6 @@
D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; }; D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; }; D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; }; D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; };
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */; };
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10E25B62D2400298D0F /* DiskCache.swift */; }; D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C10E25B62D2400298D0F /* DiskCache.swift */; };
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11425B62E9700298D0F /* CacheExpiry.swift */; }; D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11425B62E9700298D0F /* CacheExpiry.swift */; };
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */; }; D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */; };
@ -636,6 +636,7 @@
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = "<group>"; }; D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseEmojiLabel.swift; sourceTree = "<group>"; };
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = "<group>"; }; D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = "<group>"; };
D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainActor+Unsafe.swift"; sourceTree = "<group>"; };
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = "<group>"; }; D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = "<group>"; };
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; }; D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
@ -668,7 +669,6 @@
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; }; D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; }; D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; }; D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTablePrefetching.swift; sourceTree = "<group>"; };
D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = "<group>"; }; D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = "<group>"; };
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; }; D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; }; D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
@ -1316,6 +1316,7 @@
D61F758F29353B4300C0B37F /* FileManager+Size.swift */, D61F758F29353B4300C0B37F /* FileManager+Size.swift */,
D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */, D61F75AC293AF39000C0B37F /* Filter+Helpers.swift */,
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */, D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */,
D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1495,7 +1496,6 @@
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */, D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */, D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */, D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */,
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */, D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */, D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */, D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
@ -2200,6 +2200,7 @@
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */, D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */, D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */,
D6B81F442560390300F6E31D /* MenuController.swift in Sources */, D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */, D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */, D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */, D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
@ -2260,7 +2261,6 @@
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */, D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */, D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */, D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */, D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */,
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */, D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */, D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,

View File

@ -13,11 +13,11 @@ import Pachyderm
class CreateListService { class CreateListService {
private let mastodonController: MastodonController private let mastodonController: MastodonController
private let present: (UIViewController) -> Void private let present: (UIViewController) -> Void
private let didCreateList: (@MainActor (List) -> Void)? private let didCreateList: (@MainActor (List) async -> Void)?
private var createAction: UIAlertAction? private var createAction: UIAlertAction?
init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) -> Void)?) { init(mastodonController: MastodonController, present: @escaping (UIViewController) -> Void, didCreateList: (@MainActor (List) async -> Void)?) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.present = present self.present = present
self.didCreateList = didCreateList self.didCreateList = didCreateList
@ -50,7 +50,7 @@ class CreateListService {
let request = Client.createList(title: title) let request = Client.createList(title: title)
let (list, _) = try await mastodonController.run(request) let (list, _) = try await mastodonController.run(request)
mastodonController.addedList(list) mastodonController.addedList(list)
self.didCreateList?(list) await self.didCreateList?(list)
} catch { } catch {
let alert = UIAlertController(title: "Error Creating List", message: error.localizedDescription, preferredStyle: .alert) let alert = UIAlertController(title: "Error Creating List", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))

View File

@ -101,6 +101,10 @@ struct InstanceFeatures {
hasMastodonVersion(3, 5, 0) hasMastodonVersion(3, 5, 0)
} }
var pollVotersCount: Bool {
instanceType.isMastodon
}
mutating func update(instance: Instance, nodeInfo: NodeInfo?) { mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
let ver = instance.version.lowercased() let ver = instance.version.lowercased()
if ver.contains("glitch") { if ver.contains("glitch") {

View File

@ -40,7 +40,7 @@ class MastodonController: ObservableObject {
} }
private let transient: Bool private let transient: Bool
private(set) lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient) private(set) nonisolated lazy var persistentContainer = MastodonCachePersistentStore(for: accountInfo, transient: transient)
let instanceURL: URL let instanceURL: URL
var accountInfo: LocalData.UserAccountInfo? var accountInfo: LocalData.UserAccountInfo?
@ -110,7 +110,7 @@ class MastodonController: ObservableObject {
return response return response
} }
func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) { func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
let response = await runResponse(request) let response = await runResponse(request)
try Task.checkCancellation() try Task.checkCancellation()
switch response { switch response {
@ -181,7 +181,7 @@ class MastodonController: ObservableObject {
_ = try await (ownAccount, ownInstance) _ = try await (ownAccount, ownInstance)
loadLists() loadLists()
async let _ = await loadFilters() _ = await loadFilters()
} catch { } catch {
Logging.general.error("MastodonController initialization failed: \(String(describing: error))") Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
} }

View File

@ -39,9 +39,9 @@ class OpenInSafariActivity: UIActivity {
static func completionHandler(navigator: TuskerNavigationDelegate, url: URL) -> UIActivityViewController.CompletionWithItemsHandler { static func completionHandler(navigator: TuskerNavigationDelegate, url: URL) -> UIActivityViewController.CompletionWithItemsHandler {
return { (activityType, _, _, _) in return { (activityType, _, _, _) in
if activityType == .openInSafari { if activityType == .openInSafari {
let vc = SFSafariViewController(url: url) MainActor.runUnsafely {
vc.preferredControlTintColor = Preferences.shared.accentColor.color navigator.selected(url: url, allowResolveStatuses: false, allowUniversalLinks: false)
navigator.show(vc) }
} }
} }
} }

View File

@ -26,6 +26,10 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
} }
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
guard #unavailable(iOS 16.4) else {
// iOS 16.4 shows the full content and attachments in the Messages preview, better than what we can generate with LPLinkMetadata
return nil
}
let metadata = LPLinkMetadata() let metadata = LPLinkMetadata()
metadata.originalURL = status.url! metadata.originalURL = status.url!
metadata.url = status.url! metadata.url = status.url!

View File

@ -151,6 +151,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
original(self, selector, sender) original(self, selector, sender)
} }
} }
if let exception {
SentrySDK.capture(exception: exception)
}
} as @convention(block) (UIStatusBarManager, AnyObject) -> Void) } as @convention(block) (UIStatusBarManager, AnyObject) -> Void)
originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@") originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@")
if originalIMP == nil { if originalIMP == nil {

View File

@ -77,6 +77,7 @@ class ImageDataCache {
try? disk?.removeAll() try? disk?.removeAll()
} }
// TODO: consider removing this and letting ImageCache just use the UIImage thumbnailing API
private func scaleImageIfDesired(data: Data) -> UIImage? { private func scaleImageIfDesired(data: Data) -> UIImage? {
guard let desiredPixelSize = desiredPixelSize, guard let desiredPixelSize = desiredPixelSize,
let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else { let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else {
@ -84,14 +85,14 @@ class ImageDataCache {
} }
let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height) let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height)
let downsampleOptions = [ let downsampleOptions: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true, kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimension kCGImageSourceThumbnailMaxPixelSize: maxDimension
] as CFDictionary ]
if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) { if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions as CFDictionary) {
return UIImage(cgImage: downsampled) return UIImage(cgImage: downsampled)
} else { } else {
return nil return nil

View File

@ -0,0 +1,55 @@
//
// MainActor+Unsafe.swift
// Tusker
//
// Created by Shadowfacts on 2/19/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import Foundation
/*
Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/fe3b3fd5436b196d8c5211ab2cc4b69fc35524fe/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift
Copyright (c) 2022, Chime
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public extension MainActor {
/// Execute the given body closure on the main actor without enforcing MainActor isolation.
///
/// This function exists to work around libraries with incorrect/inconsistent concurrency annotations. You should be **extremely** careful when using it, and only as a last resort.
///
/// It will crash if run on any non-main thread.
@MainActor(unsafe)
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
dispatchPrecondition(condition: .onQueue(.main))
return try body()
}
}

View File

@ -11,7 +11,7 @@ import Pachyderm
import Combine import Combine
/// An opaque object that serves as the cache for the filtered-ness of a particular status. /// An opaque object that serves as the cache for the filtered-ness of a particular status.
class FilterState { class FilterState: @unchecked Sendable {
static var unknown: FilterState { FilterState(state: .unknown) } static var unknown: FilterState { FilterState(state: .unknown) }
fileprivate var state: State fileprivate var state: State

View File

@ -128,7 +128,9 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
container.navigationDelegate.showMoreOptions(forStatus: status.id, source: .view(container)) MainActor.runUnsafely {
container.navigationDelegate.showMoreOptions(forStatus: status.id, source: .view(container))
}
completion(true) completion(true)
} }
// bold to more closesly match other action symbols // bold to more closesly match other action symbols
@ -166,7 +168,9 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction
private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in
container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false) MainActor.runUnsafely {
container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false)
}
completion(true) completion(true)
} }
action.image = UIImage(systemName: "safari") action.image = UIImage(systemName: "safari")

View File

@ -49,7 +49,7 @@ class SavedDataManager: Codable {
var changed = false var changed = false
if let hashtags = savedHashtags[accountID] { if let hashtags = savedHashtags[accountID] {
let objects = hashtags.map { let objects: [[String: Any]] = hashtags.map {
["url": $0.url, "name": $0.name] ["url": $0.url, "name": $0.name]
} }
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects) let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)

View File

@ -7,6 +7,7 @@
// //
import UIKit import UIKit
import Sentry
protocol TuskerSceneDelegate: UISceneDelegate { protocol TuskerSceneDelegate: UISceneDelegate {
var window: UIWindow? { get } var window: UIWindow? { get }
@ -31,11 +32,14 @@ extension TuskerSceneDelegate {
guard let window else { return } guard let window else { return }
window.overrideUserInterfaceStyle = Preferences.shared.theme window.overrideUserInterfaceStyle = Preferences.shared.theme
window.tintColor = Preferences.shared.accentColor.color window.tintColor = Preferences.shared.accentColor.color
_ = catchNSException { let exception = catchNSException {
let key = ["Controller", "Presentation", "root", "_"].reversed().joined() let key = ["Controller", "Presentation", "root", "_"].reversed().joined()
if let rootPresentationController = window.value(forKey: key) as? UIPresentationController { if let rootPresentationController = window.value(forKey: key) as? UIPresentationController {
rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode) rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode)
} }
} }
if let exception {
SentrySDK.capture(exception: exception)
}
} }
} }

View File

@ -102,7 +102,7 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
} }
} }
private func request(for range: RequestRange) -> Request<[Account]> { private nonisolated func request(for range: RequestRange) -> Request<[Account]> {
switch mode { switch mode {
case .following: case .following:
return Account.getFollowing(accountID, range: range.withCount(Self.pageSize)) return Account.getFollowing(accountID, range: range.withCount(Self.pageSize))

View File

@ -8,8 +8,8 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import AVFoundation @preconcurrency import AVFoundation
import VisionKit @preconcurrency import VisionKit
protocol LargeImageContentView: UIView { protocol LargeImageContentView: UIView {
var animationImage: UIImage? { get } var animationImage: UIImage? { get }

View File

@ -29,6 +29,37 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
^[[ConcurrencyPlus](https://github.com/ChimeHQ/ConcurrencyPlus)](headingLevel: 2)
BSD 3-Clause License
Copyright (c) 2022, Chime
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
^[[SwiftSoup](https://github.com/scinfu/swiftsoup)](headingLevel: 2) ^[[SwiftSoup](https://github.com/scinfu/swiftsoup)](headingLevel: 2)
Copyright (c) 2016 Nabil Chatbi Copyright (c) 2016 Nabil Chatbi

View File

@ -79,12 +79,11 @@ struct AdvancedPrefsView : View {
} }
.appGroupedListRowBackground() .appGroupedListRowBackground()
.task { .task {
CKContainer.default().accountStatus { status, error in do {
if let error { let status = try await CKContainer.default().accountStatus()
Logging.general.error("Unable to get CloudKit status: \(String(describing: error))") self.cloudKitStatus = status
} else { } catch {
self.cloudKitStatus = status Logging.general.error("Unable to get CloudKit status: \(String(describing: error))")
}
} }
} }
} }

View File

@ -12,6 +12,7 @@ import Pachyderm
import WebURLFoundationExtras import WebURLFoundationExtras
import SwiftUI import SwiftUI
@MainActor
protocol MenuActionProvider: AnyObject { protocol MenuActionProvider: AnyObject {
var navigationDelegate: TuskerNavigationDelegate? { get } var navigationDelegate: TuskerNavigationDelegate? { get }
var toastableViewController: ToastableViewController? { get } var toastableViewController: ToastableViewController? { get }
@ -86,10 +87,9 @@ extension MenuActionProvider {
self.navigationDelegate!.present($0, animated: true) self.navigationDelegate!.present($0, animated: true)
}) { list in }) { list in
let req = List.add(list, accounts: [accountID]) let req = List.add(list, accounts: [accountID])
mastodonController.run(req) { response in let response = await mastodonController.runResponse(req)
if case .failure(let error) = response { if case .failure(let error) = response {
self.handleError(error, title: "Error Adding to List") self.handleError(error, title: "Error Adding to List")
}
} }
} }
service.run() service.run()

View File

@ -1,39 +0,0 @@
//
// StatusTablePrefetching.swift
// Tusker
//
// Created by Shadowfacts on 1/18/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import Foundation
import CoreData
protocol StatusTablePrefetching: TuskerNavigationDelegate {
}
extension StatusTablePrefetching {
func prefetchStatuses(with ids: [String]) {
let context = apiController.persistentContainer.prefetchBackgroundContext
context.perform {
guard let statuses = getStatusesWith(ids: ids, in: context) else {
return
}
for status in statuses {
guard let avatar = status.account.avatar else { continue }
ImageCache.avatars.fetchIfNotCached(avatar)
for attachment in status.attachments where attachment.kind == .image {
ImageCache.attachments.fetchIfNotCached(attachment.url)
}
}
}
}
}
fileprivate func getStatusesWith(ids: [String], in context: NSManagedObjectContext) -> [StatusMO]? {
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
request.predicate = NSPredicate(format: "id IN %@", ids)
return try? context.fetch(request)
}

View File

@ -23,17 +23,18 @@ protocol TimelineLikeCollectionViewController: UIViewController, TimelineLikeCon
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get } var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get }
} }
protocol TimelineLikeCollectionViewSection: Hashable { protocol TimelineLikeCollectionViewSection: Hashable, Sendable {
static var entries: Self { get } static var entries: Self { get }
static var footer: Self { get } static var footer: Self { get }
} }
protocol TimelineLikeCollectionViewItem: Hashable { protocol TimelineLikeCollectionViewItem: Hashable, Sendable {
associatedtype TimelineItem associatedtype TimelineItem
static var loadingIndicator: Self { get } static var loadingIndicator: Self { get }
static var confirmLoadMore: Self { get } static var confirmLoadMore: Self { get }
@MainActor
static func fromTimelineItem(_ item: TimelineItem) -> Self static func fromTimelineItem(_ item: TimelineItem) -> Self
} }

View File

@ -10,7 +10,7 @@ import Foundation
import OSLog import OSLog
protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject { protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
associatedtype TimelineItem associatedtype TimelineItem: Sendable
func loadInitial() async throws -> [TimelineItem] func loadInitial() async throws -> [TimelineItem]
@ -37,7 +37,7 @@ protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "TimelineLikeController") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "TimelineLikeController")
@MainActor @MainActor
class TimelineLikeController<Item> { class TimelineLikeController<Item: Sendable> {
private unowned var delegate: any TimelineLikeControllerDelegate<Item> private unowned var delegate: any TimelineLikeControllerDelegate<Item>

View File

@ -10,6 +10,7 @@ import UIKit
import SafariServices import SafariServices
import Pachyderm import Pachyderm
@MainActor
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController { protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
var apiController: MastodonController! { get } var apiController: MastodonController! { get }
} }

View File

@ -90,8 +90,10 @@ extension BaseEmojiLabel {
// even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878) // even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878)
// so, just ignore the warnings // so, just ignore the warnings
let emojiAttachments = emojiImages.withLock { let emojiAttachments = emojiImages.withLock {
$0.mapValues { image in let emojiFont = self.emojiFont
NSTextAttachment(emojiImage: image, in: self.emojiFont, with: self.emojiTextColor) let emojiTextColor = self.emojiTextColor
return $0.mapValues { image in
NSTextAttachment(emojiImage: image, in: emojiFont, with: emojiTextColor)
} }
} }
let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil

View File

@ -15,7 +15,7 @@ class PollOptionView: UIView {
let checkbox: PollOptionCheckboxView let checkbox: PollOptionCheckboxView
init(poll: Poll, option: Poll.Option) { init(poll: Poll, option: Poll.Option, mastodonController: MastodonController) {
checkbox = PollOptionCheckboxView(multiple: poll.multiple) checkbox = PollOptionCheckboxView(multiple: poll.multiple)
super.init(frame: .zero) super.init(frame: .zero)
@ -48,7 +48,14 @@ class PollOptionView: UIView {
if (poll.voted ?? false) || poll.effectiveExpired, if (poll.voted ?? false) || poll.effectiveExpired,
let optionVotes = option.votesCount { let optionVotes = option.votesCount {
let frac = poll.votesCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(poll.votesCount) let frac: CGFloat
if poll.multiple,
let votersCount = poll.votersCount,
mastodonController.instanceFeatures.pollVotersCount {
frac = votersCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(votersCount)
} else {
frac = poll.votesCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(poll.votesCount)
}
let percent = String(format: "%.0f%%", frac * 100) let percent = String(format: "%.0f%%", frac * 100)
percentLabel.isHidden = false percentLabel.isHidden = false

View File

@ -10,6 +10,8 @@ import UIKit
import Pachyderm import Pachyderm
class PollOptionsView: UIControl { class PollOptionsView: UIControl {
var mastodonController: MastodonController!
var checkedOptionIndices: [Int] { var checkedOptionIndices: [Int] {
options.enumerated().filter { $0.element.checkbox.isChecked }.map(\.offset) options.enumerated().filter { $0.element.checkbox.isChecked }.map(\.offset)
@ -62,7 +64,7 @@ class PollOptionsView: UIControl {
options.forEach { $0.removeFromSuperview() } options.forEach { $0.removeFromSuperview() }
options = poll.options.enumerated().map { (index, opt) in options = poll.options.enumerated().map { (index, opt) in
let optionView = PollOptionView(poll: poll, option: opt) let optionView = PollOptionView(poll: poll, option: opt, mastodonController: mastodonController)
optionView.checkbox.readOnly = !isEnabled optionView.checkbox.readOnly = !isEnabled
optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
optionView.checkbox.voted = poll.voted ?? false optionView.checkbox.voted = poll.voted ?? false

View File

@ -101,6 +101,7 @@ class StatusPollView: UIView {
canVote = true canVote = true
} }
optionsView.mastodonController = mastodonController
optionsView.isEnabled = canVote optionsView.isEnabled = canVote
optionsView.updateUI(poll: poll) optionsView.updateUI(poll: poll)

View File

@ -266,6 +266,7 @@ private class ProfileFieldValueView: UIView {
} }
} }
@MainActor
private struct ProfileFieldVerificationView: View { private struct ProfileFieldVerificationView: View {
let acct: String let acct: String
let verifiedAt: Date let verifiedAt: Date