Compare commits

..

No commits in common. "e4eff2d362b7e7921e82cc35aaaea1bf6244549e" and "53d43b570749f9072ea0662ffafd8b7db630b132" have entirely different histories.

39 changed files with 458 additions and 711 deletions

View File

@ -1,20 +1,5 @@
# Changelog
## 2023.8 (105)
Features/Improvements:
- Use server-set preference for default post visibility, language, and (on Hometown) local-only
- Add preference to underline links
- Allow changing list reply policy and exclusivity from menu on Edit List screen
- Attribute network requests to user, rather than developer, when appropriate
Bugfixes:
- Fix older notifications not loading if all initially-loaded are grouped together
- Fix list timelines failing to refresh if there were no statuses initially
- Fix timeline jump button having a background when Button Shapes accessibility setting is on
- Fix crash when relaunching app after not being launched in more than a week
- Fix potential crash on instance selector screen
- Fix crash when showing display names with custom emojis in certain places
## 2023.8 (104)
Features/Improvements:
- Show search operators on Mastodon 4.2

View File

@ -81,7 +81,6 @@ public class DraftsPersistentContainer: NSPersistentContainer {
contentWarning: String,
inReplyToID: String?,
visibility: Visibility,
language: String?,
localOnly: Bool
) -> Draft {
let draft = Draft(context: viewContext)
@ -93,7 +92,6 @@ public class DraftsPersistentContainer: NSPersistentContainer {
draft.contentWarningEnabled = !contentWarning.isEmpty
draft.inReplyToID = inReplyToID
draft.visibility = visibility
draft.language = language
draft.localOnly = localOnly
save()
return draft

View File

@ -171,18 +171,6 @@ public class InstanceFeatures: ObservableObject {
hasMastodonVersion(4, 2, 0)
}
public var hasServerPreferences: Bool {
hasMastodonVersion(2, 8, 0)
}
public var listRepliesPolicy: Bool {
hasMastodonVersion(3, 3, 0)
}
public var exclusiveLists: Bool {
hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil))
}
public init() {
}

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "Pachyderm",
platforms: [
.iOS(.v15),
.iOS(.v14),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.

View File

@ -105,20 +105,6 @@ public class Client {
return task
}
@discardableResult
public func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
return try await withCheckedThrowingContinuation { continuation in
run(request) { response in
switch response {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let result, let pagination):
continuation.resume(returning: (result, pagination))
}
}
}
}
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
components.path = request.endpoint.path
@ -136,8 +122,6 @@ public class Client {
}
if let accessToken = accessToken {
urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
// We consider authenticated requests to be user-initiated.
urlRequest.attribution = .user
}
return urlRequest
}
@ -239,10 +223,6 @@ public class Client {
return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis")
}
public static func getPreferences() -> Request<Preferences> {
return Request(method: .get, path: "/api/v1/preferences")
}
// MARK: - Accounts
public static func getAccount(id: String) -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/\(id)")

View File

@ -11,18 +11,14 @@ import Foundation
public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
public let id: String
public let title: String
public let replyPolicy: ReplyPolicy?
public let exclusive: Bool?
public var timeline: Timeline {
return .list(id: id)
}
public init(id: String, title: String, replyPolicy: ReplyPolicy?, exclusive: Bool?) {
public init(id: String, title: String) {
self.id = id
self.title = title
self.replyPolicy = replyPolicy
self.exclusive = exclusive
}
public static func ==(lhs: List, rhs: List) -> Bool {
@ -40,15 +36,8 @@ public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
return request
}
public static func update(_ listID: String, title: String, replyPolicy: ReplyPolicy?, exclusive: Bool?) -> Request<List> {
var params = ["title" => title]
if let replyPolicy {
params.append("replies_policy" => replyPolicy.rawValue)
}
if let exclusive {
params.append("exclusive" => exclusive)
}
return Request<List>(method: .put, path: "/api/v1/lists/\(listID)", body: ParametersBody(params))
public static func update(_ listID: String, title: String) -> Request<List> {
return Request<List>(method: .put, path: "/api/v1/lists/\(listID)", body: ParametersBody(["title" => title]))
}
public static func delete(_ listID: String) -> Request<Empty> {
@ -70,13 +59,5 @@ public struct List: ListProtocol, Decodable, Equatable, Hashable, Sendable {
private enum CodingKeys: String, CodingKey {
case id
case title
case replyPolicy = "replies_policy"
case exclusive
}
}
extension List {
public enum ReplyPolicy: String, Codable, Hashable, CaseIterable, Sendable {
case followed, list, none
}
}

View File

@ -1,37 +0,0 @@
//
// Preferences.swift
// Pachyderm
//
// Created by Shadowfacts on 10/26/23.
//
import Foundation
public struct Preferences: Codable, Sendable {
public let postingDefaultVisibility: Visibility
public let postingDefaultSensitive: Bool
public let postingDefaultLanguage: String
// Whether posts federate or not (local-only) on Hometown
public let postingDefaultFederation: Bool?
public let readingExpandMedia: ExpandMedia
public let readingExpandSpoilers: Bool
public let readingAutoplayGifs: Bool
enum CodingKeys: String, CodingKey {
case postingDefaultVisibility = "posting:default:visibility"
case postingDefaultSensitive = "posting:default:sensitive"
case postingDefaultLanguage = "posting:default:language"
case postingDefaultFederation = "posting:default:federation"
case readingExpandMedia = "reading:expand:media"
case readingExpandSpoilers = "reading:expand:spoilers"
case readingAutoplayGifs = "reading:autoplay:gifs"
}
}
extension Preferences {
public enum ExpandMedia: String, Codable, Sendable {
case `default`
case always = "show_all"
case never = "hide_all"
}
}

View File

@ -10,6 +10,4 @@ import Foundation
public protocol ListProtocol {
var id: String { get }
var title: String { get }
var replyPolicy: List.ReplyPolicy? { get }
var exclusive: Bool? { get }
}

View File

@ -1,97 +0,0 @@
//
// PostVisibility.swift
// TuskerPreferences
//
// Created by Shadowfacts on 10/26/23.
//
import Foundation
import Pachyderm
public enum PostVisibility: Codable, Hashable, CaseIterable {
case serverDefault
case visibility(Visibility)
public static var allCases: [PostVisibility] = [.serverDefault] + Visibility.allCases.map { .visibility($0) }
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
switch self {
case .serverDefault:
// If the server doesn't have a default visibility preference, we fallback to public.
// This isn't ideal, but I don't want to add a separate preference for "Default Post Visibility Fallback" :/
serverDefault ?? .public
case .visibility(let vis):
vis
}
}
public func resolved(withServerDefault serverDefault: () async -> Visibility?) async -> Visibility {
switch self {
case .serverDefault:
await serverDefault() ?? .public
case .visibility(let vis):
vis
}
}
public var displayName: String {
switch self {
case .serverDefault:
return "Account Default"
case .visibility(let vis):
return vis.displayName
}
}
public var imageName: String? {
switch self {
case .serverDefault:
return nil
case .visibility(let vis):
return vis.imageName
}
}
}
public enum ReplyVisibility: Codable, Hashable, CaseIterable {
case sameAsPost
case visibility(Visibility)
public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
switch self {
case .sameAsPost:
Preferences.shared.defaultPostVisibility.resolved(withServerDefault: serverDefault)
case .visibility(let vis):
vis
}
}
public func resolved(withServerDefault serverDefault: () async -> Visibility?) async -> Visibility {
switch self {
case .sameAsPost:
await Preferences.shared.defaultPostVisibility.resolved(withServerDefault: serverDefault)
case .visibility(let vis):
vis
}
}
public var displayName: String {
switch self {
case .sameAsPost:
return "Same as Default"
case .visibility(let vis):
return vis.displayName
}
}
public var imageName: String? {
switch self {
case .sameAsPost:
return nil
case .visibility(let vis):
return vis.imageName
}
}
}

View File

@ -61,13 +61,8 @@ public final class Preferences: Codable, ObservableObject {
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
self.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode
self.underlineTextLinks = try container.decodeIfPresent(Bool.self, forKey: .underlineTextLinks) ?? false
if let existing = try? container.decode(Visibility.self, forKey: .defaultPostVisibility) {
self.defaultPostVisibility = .visibility(existing)
} else {
self.defaultPostVisibility = try container.decode(PostVisibility.self, forKey: .defaultPostVisibility)
}
self.defaultPostVisibility = try container.decode(Visibility.self, forKey: .defaultPostVisibility)
self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
@ -126,7 +121,6 @@ public final class Preferences: Codable, ObservableObject {
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
try container.encode(widescreenNavigationMode, forKey: .widescreenNavigationMode)
try container.encode(underlineTextLinks, forKey: .underlineTextLinks)
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(defaultReplyVisibility, forKey: .defaultReplyVisibility)
@ -181,10 +175,9 @@ public final class Preferences: Codable, ObservableObject {
@Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
private static var defaultWidescreenNavigationMode = WidescreenNavigationMode.splitScreen
@Published public var widescreenNavigationMode = Preferences.defaultWidescreenNavigationMode
@Published public var underlineTextLinks = false
// MARK: Composing
@Published public var defaultPostVisibility = PostVisibility.serverDefault
@Published public var defaultPostVisibility = Visibility.public
@Published public var defaultReplyVisibility = ReplyVisibility.sameAsPost
@Published public var requireAttachmentDescriptions = false
@Published public var contentWarningCopyMode = ContentWarningCopyMode.asIs
@ -252,7 +245,6 @@ public final class Preferences: Codable, ObservableObject {
case leadingStatusSwipeActions
case trailingStatusSwipeActions
case widescreenNavigationMode
case underlineTextLinks
case defaultPostVisibility
case defaultReplyVisibility
@ -296,6 +288,42 @@ public final class Preferences: Codable, ObservableObject {
}
extension Preferences {
public enum ReplyVisibility: Codable, Hashable, CaseIterable {
case sameAsPost
case visibility(Visibility)
public static var allCases: [Preferences.ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
public var resolved: Visibility {
switch self {
case .sameAsPost:
return Preferences.shared.defaultPostVisibility
case .visibility(let vis):
return vis
}
}
public var displayName: String {
switch self {
case .sameAsPost:
return "Same as Default"
case .visibility(let vis):
return vis.displayName
}
}
public var imageName: String? {
switch self {
case .sameAsPost:
return nil
case .visibility(let vis):
return vis.imageName
}
}
}
}
extension Preferences {
public enum AttachmentBlurMode: Codable, Hashable, CaseIterable {
case useStatusSetting

View File

@ -12,7 +12,6 @@ import ComposeUI
import UniformTypeIdentifiers
import TuskerPreferences
import Combine
import Pachyderm
class ShareViewController: UIViewController {
@ -51,26 +50,21 @@ class ShareViewController: UIViewController {
}
private func createDraft(account: UserAccountInfo) async -> Draft {
async let (text, attachments) = getDraftConfigurationFromExtensionContext()
// TODO: I really don't like that there's a network request in the hot path here, but we don't have easy access to AccountPreferences :/
let serverPrefs = try? await Client(baseURL: account.instanceURL, accessToken: account.accessToken).run(Client.getPreferences()).0
let visibility = Preferences.shared.defaultPostVisibility.resolved(withServerDefault: serverPrefs?.postingDefaultVisibility)
let (text, attachments) = await getDraftConfigurationFromExtensionContext()
let draft = DraftsPersistentContainer.shared.createDraft(
accountID: account.id,
text: await text,
text: text,
contentWarning: "",
inReplyToID: nil,
visibility: visibility,
language: serverPrefs?.postingDefaultLanguage,
localOnly: !(serverPrefs?.postingDefaultFederation ?? true)
visibility: Preferences.shared.defaultPostVisibility,
localOnly: false
)
for attachment in await attachments {
for attachment in attachments {
DraftsPersistentContainer.shared.viewContext.insert(attachment)
}
draft.draftAttachments = await attachments
draft.draftAttachments = attachments
return draft
}

View File

@ -214,6 +214,8 @@
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */; };
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A3812956123A0036B6EF /* TimelinePosition.swift */; };
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */; };
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */; };
D6A4531629EF64BA00032932 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4531529EF64BA00032932 /* ShareViewController.swift */; };
D6A4531929EF64BA00032932 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6A4531729EF64BA00032932 /* MainInterface.storyboard */; };
D6A4531D29EF64BA00032932 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6A4531329EF64BA00032932 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -256,6 +258,7 @@
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366C2828444F00237D0E /* SavedInstance.swift */; };
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366E2828452F00237D0E /* SavedHashtag.swift */; };
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */; };
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */; };
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; };
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; };
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
@ -264,7 +267,6 @@
D6BD395B29B64441005FFD2B /* ComposeHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */; };
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; };
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */; };
D6C041C42AED77730094D32D /* EditListSettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C041C32AED77730094D32D /* EditListSettingsService.swift */; };
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
@ -616,6 +618,8 @@
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderButton.swift; sourceTree = "<group>"; };
D6A3A3812956123A0036B6EF /* TimelinePosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePosition.swift; sourceTree = "<group>"; };
D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTableViewCell.swift; sourceTree = "<group>"; };
D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountTableViewCell.xib; sourceTree = "<group>"; };
D6A4531329EF64BA00032932 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D6A4531529EF64BA00032932 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
D6A4531829EF64BA00032932 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
@ -657,6 +661,7 @@
D6B9366C2828444F00237D0E /* SavedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedInstance.swift; sourceTree = "<group>"; };
D6B9366E2828452F00237D0E /* SavedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedHashtag.swift; sourceTree = "<group>"; };
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = "<group>"; };
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = "<group>"; };
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
@ -666,7 +671,6 @@
D6BD395C29B789D5005FFD2B /* TuskerComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerComponents; path = Packages/TuskerComponents; sourceTree = "<group>"; };
D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Duckable; path = Packages/Duckable; sourceTree = "<group>"; };
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = "<group>"; };
D6C041C32AED77730094D32D /* EditListSettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSettingsService.swift; sourceTree = "<group>"; };
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = "<group>"; };
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
@ -1292,6 +1296,8 @@
D6A3BC872321F78000FD64D5 /* Account Cell */ = {
isa = PBXGroup;
children = (
D6A3BC882321F79B00FD64D5 /* AccountTableViewCell.swift */,
D6A3BC892321F79B00FD64D5 /* AccountTableViewCell.xib */,
D6D12B55292D57E800D528E1 /* AccountCollectionViewCell.swift */,
);
path = "Account Cell";
@ -1421,6 +1427,7 @@
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
D62D67C42A97D8CD00167EE2 /* MultiColumnNavigationController.swift */,
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
@ -1638,7 +1645,6 @@
D621733228F1D5ED004C7DB1 /* ReblogService.swift */,
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
D6F6A551291F098700F496A8 /* RenameListService.swift */,
D6C041C32AED77730094D32D /* EditListSettingsService.swift */,
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */,
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */,
D61F75B0293BD85300C0B37F /* CreateFilterService.swift */,
@ -1855,6 +1861,7 @@
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
@ -1980,6 +1987,7 @@
D6ADB6F028ED1F25009924AB /* CachedImageView.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
D65B4B682977769E00DABDFB /* StatusActionAccountListViewController.swift in Sources */,
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
@ -2143,7 +2151,6 @@
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
D6C041C42AED77730094D32D /* EditListSettingsService.swift in Sources */,
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */,
@ -2220,6 +2227,7 @@
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,

View File

@ -1,46 +0,0 @@
//
// EditListSettingsService.swift
// Tusker
//
// Created by Shadowfacts on 10/28/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
@MainActor
class EditListSettingsService {
private let list: ListProtocol
private let mastodonController: MastodonController
private let present: (UIViewController) -> Void
init(list: ListProtocol, mastodonController: MastodonController, present: @escaping (UIViewController) -> Void) {
self.list = list
self.mastodonController = mastodonController
self.present = present
}
func run(title: String? = nil, replyPolicy: List.ReplyPolicy? = nil, exclusive: Bool? = nil) async {
do {
let req = List.update(
list.id,
title: title ?? list.title,
replyPolicy: replyPolicy ?? list.replyPolicy,
exclusive: exclusive ?? list.exclusive
)
let (list, _) = try await mastodonController.run(req)
mastodonController.updatedList(list)
} catch {
let alert = UIAlertController(title: "Error Updating List", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Retry", style: .default, handler: { _ in
Task {
await self.run(title: title, replyPolicy: replyPolicy, exclusive: exclusive)
}
}))
present(alert)
}
}
}

View File

@ -193,8 +193,6 @@ class MastodonController: ObservableObject {
@MainActor
func initialize() {
precondition(!transient, "Cannot initialize transient MastodonController")
// we want this to happen immediately, and synchronously so that the filters (which don't change that often)
// are available when Filterers are constructed
loadCachedFilters()
@ -219,7 +217,6 @@ class MastodonController: ObservableObject {
loadLists()
_ = await loadFilters()
await loadServerPreferences()
} catch {
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
}
@ -361,18 +358,6 @@ class MastodonController: ObservableObject {
}
}
// MainActor because the accountPreferences instance is bound to the view context
@MainActor
private func loadServerPreferences() async {
guard instanceFeatures.hasServerPreferences,
let (prefs, _) = try? await run(Client.getPreferences()) else {
return
}
accountPreferences!.serverDefaultLanguage = prefs.postingDefaultLanguage
accountPreferences!.serverDefaultVisibility = prefs.postingDefaultVisibility
accountPreferences!.serverDefaultFederation = prefs.postingDefaultFederation ?? true
}
private func updateActiveInstance(from instance: Instance) {
persistentContainer.performBackgroundTask { context in
if let existing = try? context.fetch(ActiveInstance.fetchRequest()).first {
@ -449,12 +434,16 @@ class MastodonController: ObservableObject {
guard let lists = try? persistentContainer.viewContext.fetch(req) else {
return []
}
return lists.map(\.apiList).sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
return lists.map {
List(id: $0.id, title: $0.title)
}.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
}
func getCachedList(id: String) -> List? {
let req = ListMO.fetchRequest(id: id)
return (try? persistentContainer.viewContext.fetch(req).first).map(\.apiList)
return (try? persistentContainer.viewContext.fetch(req).first).flatMap {
List(id: $0.id, title: $0.title)
}
}
@MainActor
@ -471,7 +460,7 @@ class MastodonController: ObservableObject {
}
@MainActor
func updatedList(_ list: List) {
func renamedList(_ list: List) {
var new = self.lists
if let index = new.firstIndex(where: { $0.id == list.id }) {
new[index] = list
@ -530,12 +519,8 @@ class MastodonController: ObservableObject {
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft {
var acctsToMention = [String]()
var visibility = if inReplyToID != nil {
Preferences.shared.defaultReplyVisibility.resolved(withServerDefault: accountPreferences!.serverDefaultVisibility)
} else {
Preferences.shared.defaultPostVisibility.resolved(withServerDefault: accountPreferences!.serverDefaultVisibility)
}
var localOnly = instanceFeatures.localOnlyPosts && !accountPreferences!.serverDefaultFederation
var visibility = inReplyToID != nil ? Preferences.shared.defaultReplyVisibility.resolved : Preferences.shared.defaultPostVisibility
var localOnly = false
var contentWarning = ""
if let inReplyToID = inReplyToID,
@ -574,7 +559,6 @@ class MastodonController: ObservableObject {
contentWarning: contentWarning,
inReplyToID: inReplyToID,
visibility: visibility,
language: accountPreferences!.serverDefaultLanguage,
localOnly: localOnly
)
}

View File

@ -47,9 +47,9 @@ class RenameListService {
private func updateList(with title: String) async {
do {
let req = List.update(list.id, title: title, replyPolicy: nil, exclusive: nil)
let req = List.update(list.id, title: title)
let (list, _) = try await mastodonController.run(req)
mastodonController.updatedList(list)
mastodonController.renamedList(list)
} catch {
let alert = UIAlertController(title: "Error Updating List", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))

View File

@ -59,15 +59,10 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
super.awakeFromFetch()
managedObjectContext?.perform {
self.touch()
self.lastFetchedAt = Date()
}
}
/// Update the `lastFetchedAt` date so this object isn't pruned early.
func touch() {
lastFetchedAt = Date()
}
}
extension AccountMO {

View File

@ -24,22 +24,10 @@ public final class AccountPreferences: NSManagedObject {
@NSManaged public var accountID: String
@NSManaged var createdAt: Date
@NSManaged var pinnedTimelinesData: Data?
@NSManaged var serverDefaultFederation: Bool
@NSManaged var serverDefaultLanguage: String?
@NSManaged private var serverDefaultVisibilityString: String?
@LazilyDecoding(from: \AccountPreferences.pinnedTimelinesData, fallback: AccountPreferences.defaultPinnedTimelines)
var pinnedTimelines: [PinnedTimeline]
var serverDefaultVisibility: Visibility? {
get {
serverDefaultVisibilityString.flatMap(Visibility.init(rawValue:))
}
set {
serverDefaultVisibilityString = newValue?.rawValue
}
}
static func `default`(account: UserAccountInfo, context: NSManagedObjectContext) -> AccountPreferences {
let prefs = AccountPreferences(context: context)
prefs.accountID = account.id

View File

@ -25,22 +25,6 @@ public final class ListMO: NSManagedObject, ListProtocol {
@NSManaged public var id: String
@NSManaged public var title: String
@NSManaged private var replyPolicyString: String?
@NSManaged private var exclusiveInternal: Bool
public var replyPolicy: List.ReplyPolicy? {
get {
replyPolicyString.flatMap(List.ReplyPolicy.init(rawValue:))
}
set {
replyPolicyString = newValue?.rawValue
}
}
public var exclusive: Bool? {
get { exclusiveInternal }
set { exclusiveInternal = newValue ?? false }
}
}
@ -53,16 +37,5 @@ extension ListMO {
func updateFrom(apiList list: List) {
self.id = list.id
self.title = list.title
self.replyPolicy = list.replyPolicy
self.exclusive = list.exclusive
}
var apiList: List {
List(
id: id,
title: title,
replyPolicy: replyPolicy,
exclusive: exclusive
)
}
}

View File

@ -89,15 +89,10 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
super.awakeFromFetch()
managedObjectContext?.perform {
self.touch()
self.lastFetchedAt = Date()
}
}
/// Update the `lastFetchedAt` date so this object isn't pruned early.
func touch() {
lastFetchedAt = Date()
}
}
extension StatusMO {

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Account" representedClassName="AccountMO" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="active" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@ -33,9 +33,6 @@
<attribute name="accountID" optional="YES" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="pinnedTimelinesData" optional="YES" attributeType="Binary"/>
<attribute name="serverDefaultFederation" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="serverDefaultLanguage" optional="YES" attributeType="String"/>
<attribute name="serverDefaultVisibilityString" optional="YES" attributeType="String"/>
</entity>
<entity name="ActiveInstance" representedClassName="ActiveInstance" syncable="YES">
<attribute name="configurationData" optional="YES" attributeType="Binary"/>
@ -62,9 +59,7 @@
<attribute name="url" attributeType="URI"/>
</entity>
<entity name="List" representedClassName="ListMO" syncable="YES">
<attribute name="exclusiveInternal" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="replyPolicyString" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>

View File

@ -112,7 +112,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
case .list(id: let id):
let req = ListMO.fetchRequest(id: id)
if let list = try? mastodonController.persistentContainer.viewContext.fetch(req).first {
return ListTimelineViewController(for: list.apiList, mastodonController: mastodonController)
return ListTimelineViewController(for: List(id: id, title: list.title), mastodonController: mastodonController)
} else {
return TimelineViewController(for: timeline, mastodonController: mastodonController)
}

View File

@ -25,14 +25,7 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
}
@objc func handleGesture(_ recognizer: UIPanGestureRecognizer) {
guard let recognizerSuperview = recognizer.view?.superview else {
// Assume the gesture has ended b/c we don't have a view/superview anymore.
inProgress = false
direction = nil
cancel()
return
}
let translation = recognizer.translation(in: recognizerSuperview)
let translation = recognizer.translation(in: recognizer.view!.superview!)
var progress = translation.y / 200
if let direction = direction {
progress *= direction
@ -70,7 +63,7 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
override func cancel() {
super.cancel()
viewController?.isInteractivelyAnimatingDismissal = false
viewController.isInteractivelyAnimatingDismissal = false
}
}

View File

@ -10,21 +10,18 @@ import UIKit
import Pachyderm
import Combine
class EditListAccountsViewController: UIViewController, CollectionViewController {
class EditListAccountsViewController: EnhancedTableViewController {
private var list: List
private let mastodonController: MastodonController
let mastodonController: MastodonController
private var state = State.unloaded
var changedAccounts = false
private(set) var changedAccounts = false
var dataSource: DataSource!
var nextRange: RequestRange?
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
var collectionView: UICollectionView! { view as? UICollectionView }
private var nextRange: RequestRange?
private var searchResultsController: SearchResultsViewController!
private var searchController: UISearchController!
var searchResultsController: SearchResultsViewController!
var searchController: UISearchController!
private var listRenamedCancellable: AnyCancellable?
@ -32,12 +29,13 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
self.list = list
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
super.init(style: .plain)
listChanged()
listRenamedCancellable = mastodonController.$lists
.compactMap { $0.first { $0.id == list.id } }
.removeDuplicates(by: { $0.title == $1.title })
.sink { [unowned self] in
self.list = $0
self.listChanged()
@ -48,45 +46,29 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
fatalError("init(coder:) has not been implemeneted")
}
override func loadView() {
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
var config = sectionConfig
switch dataSource.itemIdentifier(for: indexPath)! {
case .loadingIndicator:
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
case .account(id: _):
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
}
return config
}
config.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in
switch dataSource.itemIdentifier(for: indexPath) {
case .account(id: let id):
let remove = UIContextualAction(style: .destructive, title: "Remove") { [unowned self] _, _, completion in
Task {
await self.removeAccount(id: id)
completion(true)
}
}
return UISwipeActionsConfiguration(actions: [remove])
default:
return nil
}
}
let layout = UICollectionViewCompositionalLayout.list(using: config)
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.allowsSelection = false
collectionView.backgroundColor = .appGroupedBackground
dataSource = createDataSource()
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: "accountCell")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 66
tableView.allowsSelection = false
tableView.backgroundColor = .appGroupedBackground
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
guard case let .account(id) = item else { fatalError() }
let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) as! AccountTableViewCell
cell.delegate = self
cell.updateUI(accountID: id)
cell.configurationUpdateHandler = { cell, state in
cell.backgroundConfiguration = .appListGroupedCell(for: state)
}
return cell
})
dataSource.editListAccountsController = self
searchResultsController = SearchResultsViewController(mastodonController: mastodonController, scope: .people)
searchResultsController.following = true
searchResultsController.delegate = self
@ -102,54 +84,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
navigationItem.hidesSearchBarWhenScrolling = false
if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked
navigationItem.renameDelegate = self
navigationItem.titleMenuProvider = { [unowned self] suggested in
var children = suggested
children.append(contentsOf: self.listSettingsMenuElements())
return UIMenu(children: children)
}
} else {
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Edit", menu: UIMenu(children: [
// uncached so that menu always reflects the current state of the list
UIDeferredMenuElement.uncached({ [unowned self] elementHandler in
var elements = self.listSettingsMenuElements()
elements.insert(UIAction(title: "Rename…", image: UIImage(systemName: "pencil"), handler: { [unowned self] _ in
RenameListService(list: self.list, mastodonController: self.mastodonController, present: {
self.present($0, animated: true)
}).run()
}), at: 0)
elementHandler(elements)
})
]))
}
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating()
}
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, itemIdentifier in
cell.delegate = self
cell.updateUI(accountID: itemIdentifier)
cell.configurationUpdateHandler = { cell, state in
cell.backgroundConfiguration = .appListGroupedCell(for: state)
}
}
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .loadingIndicator:
return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
case .account(id: let id):
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: id)
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
clearSelectionOnAppear(animated: animated)
navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("Rename", comment: "rename list button title"), style: .plain, target: self, action: #selector(renameButtonPressed))
Task {
await loadAccounts()
@ -157,48 +94,13 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
}
private func listChanged() {
title = list.title
title = String(format: NSLocalizedString("Edit %@", comment: "edit list screen title"), list.title)
}
private func listSettingsMenuElements() -> [UIMenuElement] {
var elements = [UIMenuElement]()
if mastodonController.instanceFeatures.listRepliesPolicy {
let actions = List.ReplyPolicy.allCases.map { policy in
UIAction(title: policy.actionTitle, state: list.replyPolicy == policy ? .on : .off) { [unowned self] _ in
self.setReplyPolicy(policy)
}
}
elements.append(UIMenu(title: "Show replies…", image: UIImage(systemName: "arrowshape.turn.up.left"), children: actions))
}
if mastodonController.instanceFeatures.exclusiveLists {
let actions = [
UIAction(title: "Hidden from Home", state: list.exclusive == true ? .on : .off) { [unowned self] _ in
self.setExclusive(true)
},
UIAction(title: "Shown on Home", state: list.exclusive == false ? .on : .off) { [unowned self] _ in
self.setExclusive(false)
},
]
elements.append(UIMenu(title: "Posts from this list are…", children: actions))
}
return elements
}
@MainActor
private func loadAccounts() async {
guard state == .unloaded else { return }
state = .loading
async let results = try await mastodonController.run(List.getAccounts(list.id))
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts])
snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot)
func loadAccounts() async {
do {
let (accounts, pagination) = try await results
let request = List.getAccounts(list.id)
let (accounts, pagination) = try await mastodonController.run(request)
self.nextRange = pagination?.older
await withCheckedContinuation { continuation in
@ -207,61 +109,20 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
}
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
var snapshot = self.dataSource.snapshot()
if snapshot.indexOfSection(.accounts) == nil {
snapshot.appendSections([.accounts])
} else {
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .accounts))
}
snapshot.appendItems(accounts.map { .account(id: $0.id) })
await dataSource.apply(snapshot)
state = .loaded
} catch {
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { [unowned self] toast in
toast.dismissToast(animated: true)
await self.loadAccounts()
}
self.showToast(configuration: config, animated: true)
state = .unloaded
await dataSource.apply(.init())
}
}
private func loadNextPage() async {
guard state == .loaded,
let nextRange else { return }
state = .loading
async let results = try await mastodonController.run(List.getAccounts(list.id, range: nextRange))
let origSnapshot = dataSource.snapshot()
var snapshot = origSnapshot
snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot)
do {
let (accounts, pagination) = try await results
self.nextRange = pagination?.older
await withCheckedContinuation { continuation in
mastodonController.persistentContainer.addAll(accounts: accounts) {
continuation.resume()
}
}
var snapshot = origSnapshot
snapshot.appendItems(accounts.map { .account(id: $0.id) })
await dataSource.apply(snapshot)
state = .loaded
} catch {
let config = ToastConfiguration(from: error, with: "Error Loading Accounts", in: self) { [unowned self] toast in
toast.dismissToast(animated: true)
await self.loadNextPage()
}
self.showToast(configuration: config, animated: true)
state = .loaded
await dataSource.apply(origSnapshot)
}
}
@ -296,47 +157,43 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
}
}
private func setReplyPolicy(_ replyPolicy: List.ReplyPolicy) {
Task {
let service = EditListSettingsService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
await service.run(replyPolicy: replyPolicy)
}
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
private func setExclusive(_ exclusive: Bool) {
Task {
let service = EditListSettingsService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
await service.run(exclusive: exclusive)
}
// MARK: - Interaction
@objc func renameButtonPressed() {
RenameListService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) }).run()
}
}
extension EditListAccountsViewController {
enum State {
case unloaded
case loading
case loaded
case loadingOlder
}
}
extension EditListAccountsViewController {
enum Section: Hashable {
case accounts
}
enum Item: Hashable {
case loadingIndicator
case account(id: String)
}
class DataSource: UITableViewDiffableDataSource<Section, Item> {
weak var editListAccountsController: EditListAccountsViewController?
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete,
case let .account(id) = itemIdentifier(for: indexPath) else {
return
}
extension EditListAccountsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if state == .loaded,
indexPath.item == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
Task {
await loadNextPage()
await self.editListAccountsController?.removeAccount(id: id)
}
}
}
@ -359,29 +216,3 @@ extension EditListAccountsViewController: SearchResultsViewControllerDelegate {
}
}
}
extension EditListAccountsViewController: UINavigationItemRenameDelegate {
func navigationItem(_: UINavigationItem, shouldEndRenamingWith title: String) -> Bool {
!title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
func navigationItem(_: UINavigationItem, didEndRenamingWith title: String) {
Task {
let service = EditListSettingsService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
await service.run(title: title)
}
}
}
private extension List.ReplyPolicy {
var actionTitle: String {
switch self {
case .followed:
"To accounts you follow"
case .list:
"To other list members"
case .none:
"Never"
}
}
}

View File

@ -59,7 +59,7 @@ class ListTimelineViewController: TimelineViewController {
func presentEdit(animated: Bool) {
let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
editListAccountsController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed))
editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed))
let navController = UINavigationController(rootViewController: editListAccountsController)
present(navController, animated: animated)
}

View File

@ -214,10 +214,6 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
if item == sidebar.selectedItem {
itemNavStack = secondaryNavController.viewControllers
secondaryNavController.viewControllers = []
// Sometimes removing a VC from the viewControllers array doesn't immediately remove it's view from the hierarchy
for vc in itemNavStack {
vc.viewIfLoaded?.removeFromSuperview()
}
} else {
itemNavStack = navigationStacks[item] ?? []
navigationStacks.removeValue(forKey: item)
@ -343,11 +339,6 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
let viewControllersToMove = navController.viewControllers.dropFirst(skipFirst)
navController.viewControllers.removeLast(navController.viewControllers.count - skipFirst)
// Sometimes removing a VC from the viewControllers array doesn't immediately remove it's view from the hierarchy
for vc in viewControllersToMove {
vc.viewIfLoaded?.removeFromSuperview()
}
if let prepend = prepend {
navigationStacks[item] = [prepend] + viewControllersToMove
} else {

View File

@ -539,10 +539,6 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
let itemsInSection = collectionView.numberOfItems(inSection: indexPath.section)
if indexPath.row == itemsInSection - 1 {
Task {
// Because of grouping, all cells from the first load may fit on screen,
// in which case, we try to load older while still in the loadingInitial state.
// So, wait for that to finish before trying to load more.
await controller.finishPendingOperation()
await controller.loadOlder()
}
}

View File

@ -150,8 +150,7 @@ class InstanceSelectorTableViewController: UITableViewController {
private func updateSpecificInstance(domain: String) {
activityIndicator.startAnimating()
guard let components = parseURLComponents(input: domain),
let url = components.url else {
guard let components = parseURLComponents(input: domain) else {
var snapshot = dataSource.snapshot()
if snapshot.indexOfSection(.selected) != nil {
snapshot.deleteSections([.selected])
@ -160,6 +159,7 @@ class InstanceSelectorTableViewController: UITableViewController {
activityIndicator.stopAnimating()
return
}
let url = components.url!
let client = Client(baseURL: url, session: .appDefault)
let request = Client.getInstance()

View File

@ -124,9 +124,6 @@ struct AppearancePrefsView : View {
Toggle(isOn: $preferences.showLinkPreviews) {
Text("Show Link Previews")
}
Toggle(isOn: $preferences.underlineTextLinks) {
Text("Underline Links")
}
NavigationLink("Leading Swipe Actions") {
SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions)
.edgesIgnoringSafeArea(.all)

View File

@ -28,11 +28,9 @@ struct ComposingPrefsView: View {
var visibilitySection: some View {
Section {
Picker(selection: $preferences.defaultPostVisibility, label: Text("Default Visibility")) {
ForEach(PostVisibility.allCases, id: \.self) { visibility in
ForEach(Visibility.allCases, id: \.self) { visibility in
HStack {
if let imageName = visibility.imageName {
Image(systemName: imageName)
}
Image(systemName: visibility.imageName)
Text(visibility.displayName)
}
.tag(visibility)
@ -40,7 +38,7 @@ struct ComposingPrefsView: View {
// navbar title on the ForEach is currently incorrectly applied when the picker is not expanded, see FB6838291
}
Picker(selection: $preferences.defaultReplyVisibility, label: Text("Reply Visibility")) {
ForEach(ReplyVisibility.allCases, id: \.self) { visibility in
ForEach(Preferences.ReplyVisibility.allCases, id: \.self) { visibility in
HStack {
if let imageName = visibility.imageName {
Image(systemName: imageName)

View File

@ -20,8 +20,6 @@ class TimelineJumpButton: UIView {
var config = UIButton.Configuration.plain()
config.image = UIImage(systemName: "arrow.up")
config.contentInsets = .zero
// We don't want a background for this button, even when accessibility button shapes are enabled, because it's in the navbar.
config.background.backgroundColor = .clear
return UIButton(configuration: config)
}()

View File

@ -440,15 +440,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
let originalPositionStatusIDs = position.statusIDs
var unloaded = [String]()
for id in position.statusIDs {
if let status = mastodonController.persistentContainer.status(for: id) {
// touch the status so that, even if it's old, it doesn't get pruned when we go into the background
status.touch()
} else {
unloaded.append(id)
}
}
let unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil })
guard !unloaded.isEmpty else {
return true
}
@ -726,11 +718,10 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
@objc func refresh() {
Task { @MainActor in
if case .idle = controller.state,
!dataSource.snapshot().itemIdentifiers(inSection: .statuses).isEmpty {
await controller.loadNewer()
} else {
if case .notLoadedInitial = controller.state {
await controller.loadInitial()
} else {
await controller.loadNewer()
}
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl?.endRefreshing()
@ -1166,10 +1157,7 @@ extension TimelineViewController {
let addedItems: Bool
let statusItems = snapshot.itemIdentifiers(inSection: .statuses)
guard let gapIndex = statusItems.firstIndex(of: .gap) else {
// Not sure how this is reachable (maybe the gap cell was tapped twice and the requests raced?) but w/e
return
}
let gapIndex = statusItems.firstIndex(of: .gap)!
switch direction {
case .above:
@ -1296,9 +1284,6 @@ extension TimelineViewController: UICollectionViewDelegate {
selected(status: status.reblog?.id ?? id, state: collapseState.copy())
}
case .gap:
guard controller.state == .idle else {
return
}
let cell = collectionView.cellForItem(at: indexPath) as! TimelineGapCollectionViewCell
cell.showsIndicator = true
Task {

View File

@ -0,0 +1,92 @@
//
// EnhancedTableViewController.swift
// Tusker
//
// Created by Shadowfacts on 11/10/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import SafariServices
class EnhancedTableViewController: UITableViewController {
var dragEnabled = false
override func viewDidLoad() {
super.viewDidLoad()
if dragEnabled {
tableView.dragDelegate = self
}
}
// MARK: Table View Delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? SelectableTableViewCell {
cell.didSelectCell()
}
}
}
extension EnhancedTableViewController {
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
if let cell = tableView.cellForRow(at: indexPath) as? UITableViewCell & MenuPreviewProvider {
let cellLocation = cell.convert(point, from: tableView)
guard let (previewProvider, actionsProvider) = cell.getPreviewProviders(for: cellLocation, sourceViewController: self) else {
return nil
}
let actionProvider: UIContextMenuActionProvider = { (_) in
let suggested = self.getSuggestedContextMenuActions(tableView: tableView, indexPath: indexPath, point: point)
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: suggested + actionsProvider())
}
return UIContextMenuConfiguration(identifier: nil, previewProvider: previewProvider, actionProvider: actionProvider)
} else {
return nil
}
}
// todo: replace this with the UIKit suggested actions, if possible
@objc open func getSuggestedContextMenuActions(tableView: UITableView, indexPath: IndexPath, point: CGPoint) -> [UIAction] {
return []
}
override func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
if let viewController = animator.previewViewController {
animator.preferredCommitStyle = .pop
animator.addCompletion {
if let customPresenting = viewController as? CustomPreviewPresenting {
customPresenting.presentFromPreview(presenter: self)
} else {
self.show(viewController, sender: nil)
}
}
}
}
}
extension EnhancedTableViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let cell = tableView.cellForRow(at: indexPath) as? DraggableTableViewCell else {
return []
}
return cell.dragItemsForBeginning(session: session)
}
}
extension EnhancedTableViewController: TabBarScrollableViewController {
func tabBarScrollToTop() {
tableView.scrollToTop()
}
}
extension EnhancedTableViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
tableView.scrollToTop()
return .stop
}
}

View File

@ -211,9 +211,11 @@ extension TimelineLikeCollectionViewController {
extension TimelineLikeCollectionViewController {
// apply(_:animatingDifferences:) is marked as nonisolated, so just awaiting it doesn't dispatch to the main thread, unlike other async @MainActor methods
// but we always want to update the data source on the main thread for consistency, so this method does that
@MainActor
func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool) async {
await self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
let task = Task { @MainActor in
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
await task.value
}
@MainActor

View File

@ -8,7 +8,6 @@
import Foundation
import OSLog
import Combine
protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
associatedtype TimelineItem: Sendable
@ -43,7 +42,7 @@ class TimelineLikeController<Item: Sendable> {
private unowned var delegate: any TimelineLikeControllerDelegate<Item>
private let ownerType: String
@AsyncObservable private(set) var state = State.notLoadedInitial {
private(set) var state = State.notLoadedInitial {
willSet {
guard state.canTransition(to: newValue) else {
logger.error("\(self.ownerType, privacy: .public) State \(self.state.debugDescription, privacy: .public) cannot transition to \(newValue.debugDescription, privacy: .public)")
@ -58,19 +57,6 @@ class TimelineLikeController<Item: Sendable> {
self.ownerType = ownerType
}
/// Waits for the controller to finish the current operation and arrive at the idle state.
///
/// If the current state is `notLoadedInitial`, this will wait until the controller
/// settles after the initial load.
func finishPendingOperation() async {
guard state != .idle else {
return
}
for await state in $state where state == .idle {
break
}
}
func loadInitial() async {
guard state == .notLoadedInitial || state == .idle else {
return
@ -383,17 +369,3 @@ enum TimelineGapDirection {
}
}
}
// I would love to be able to do this with @Observable, but it's not clear how to do so.
@propertyWrapper
private class AsyncObservable<Value>: ObservableObject {
@Published var wrappedValue: Value
var projectedValue: AsyncPublisher<Published<Value>.Publisher> {
$wrappedValue.values
}
init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}

View File

@ -0,0 +1,130 @@
//
// AccountTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 9/5/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import SwiftSoup
class AccountTableViewCell: UITableViewCell {
weak var delegate: (TuskerNavigationDelegate & MenuActionProvider)?
var mastodonController: MastodonController! { delegate?.apiController }
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var noteLabel: EmojiLabel!
var accountID: String!
private var avatarRequest: ImageCache.Request?
private var isGrayscale = false
override func awakeFromNib() {
super.awakeFromNib()
avatarImageView.layer.masksToBounds = true
avatarImageView.layer.cornerCurve = .continuous
usernameLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .light))
usernameLabel.adjustsFontForContentSizeCategory = true
noteLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
noteLabel.adjustsFontForContentSizeCategory = true
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPrefrences), name: .preferencesChanged, object: nil)
}
@objc func updateUIForPrefrences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
// this table view cell could be cached in a table view (e.g., SearchResultsViewController) for an account that's since been purged
return
}
displayNameLabel.updateForAccountDisplayName(account: account)
if isGrayscale != Preferences.shared.grayscaleImages {
updateGrayscaleableUI(account: account)
}
}
func updateUI(accountID: String) {
self.accountID = accountID
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
fatalError("Missing cached account \(accountID)")
}
usernameLabel.text = "@\(account.acct)"
updateGrayscaleableUI(account: account)
updateUIForPrefrences()
}
private func updateGrayscaleableUI(account: AccountMO) {
isGrayscale = Preferences.shared.grayscaleImages
let accountID = self.accountID
avatarImageView.image = nil
if let avatarURL = account.avatar {
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (_, image) in
guard let self = self else { return }
self.avatarRequest = nil
guard let image = image,
self.accountID == accountID,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { return }
DispatchQueue.main.async {
self.avatarImageView.image = transformedImage
}
}
}
let doc = try! SwiftSoup.parse(account.note)
noteLabel.text = try! doc.text()
noteLabel.setEmojis(account.emojis, identifier: account.id)
}
override func prepareForReuse() {
super.prepareForReuse()
avatarRequest?.cancel()
}
}
extension AccountTableViewCell: SelectableTableViewCell {
func didSelectCell() {
delegate?.selected(account: accountID)
}
}
extension AccountTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
guard let mastodonController = mastodonController else { return nil }
return (
content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) },
actions: { self.delegate?.actionsForProfile(accountID: self.accountID, source: .view(self.avatarImageView)) ?? [] }
)
}
}
extension AccountTableViewCell: DraggableTableViewCell {
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
guard let account = mastodonController.persistentContainer.account(for: accountID),
let currentAccountID = mastodonController.accountInfo?.id else {
return []
}
let provider = NSItemProvider(object: account.url as NSURL)
let activity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
activity.displaysAuxiliaryScene = true
provider.registerObject(activity, visibility: .all)
return [UIDragItem(itemProvider: provider)]
}
}

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="100" id="KGk-i7-Jjw" customClass="AccountTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Rp2-O5-Vew">
<rect key="frame" x="16" y="8" width="50" height="50"/>
<constraints>
<constraint firstAttribute="width" secondItem="Rp2-O5-Vew" secondAttribute="height" multiplier="1:1" id="1AQ-lU-ptd"/>
<constraint firstAttribute="height" constant="50" id="NqI-m0-owe"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5">
<rect key="frame" x="74" y="11" width="230" height="78"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fhc-bZ-lkB" customClass="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="230" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JMo-QH-1is">
<rect key="frame" x="0.0" y="20.5" width="230" height="18"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Note" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bNO-qR-YEe" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="38.5" width="230" height="39.5"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="bottomMargin" secondItem="Iif-9m-vM5" secondAttribute="bottom" id="dV0-Vm-DUb"/>
<constraint firstItem="Iif-9m-vM5" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="ihr-er-kLO"/>
<constraint firstAttribute="trailingMargin" secondItem="Iif-9m-vM5" secondAttribute="trailing" id="q7a-DT-WPF"/>
<constraint firstItem="Iif-9m-vM5" firstAttribute="leading" secondItem="Rp2-O5-Vew" secondAttribute="trailing" constant="8" id="sk1-KY-Ttj"/>
<constraint firstItem="Rp2-O5-Vew" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="xpB-wY-5d6"/>
<constraint firstItem="Rp2-O5-Vew" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="yd4-AU-qbj"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="avatarImageView" destination="Rp2-O5-Vew" id="3Gw-Xg-bd5"/>
<outlet property="displayNameLabel" destination="Fhc-bZ-lkB" id="1b0-3k-KR8"/>
<outlet property="noteLabel" destination="bNO-qR-YEe" id="4oO-c0-BOT"/>
<outlet property="usernameLabel" destination="JMo-QH-1is" id="ElX-ua-xcQ"/>
</connections>
<point key="canvasLocation" x="173.91304347826087" y="35.491071428571423"/>
</tableViewCell>
</objects>
<resources>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@ -38,7 +38,6 @@ struct AccountDisplayNameView: View {
let matches = emojiRegex.matches(in: account.displayName, options: [], range: fullRange)
guard !matches.isEmpty else { return }
let emojiSize = self.emojiSize
let emojiImages = MultiThreadDictionary<String, Image>()
let group = DispatchGroup()

View File

@ -12,7 +12,6 @@ import Pachyderm
import SafariServices
import WebURL
import WebURLFoundationExtras
import Combine
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
private let dataDetectorsScheme = "x-apple-data-detectors"
@ -53,8 +52,6 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
// The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing.
private weak var currentTargetedPreview: UITargetedPreview?
private var underlineTextLinksCancellable: AnyCancellable?
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
@ -81,30 +78,10 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
linkTextAttributes = [
.foregroundColor: UIColor.tintColor
]
updateLinkUnderlineStyle()
// the text view's builtin link interaction code is tied to isSelectable, so we need to use our own tap recognizer
let recognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped(_:)))
addGestureRecognizer(recognizer)
NotificationCenter.default.addObserver(self, selector: #selector(_updateLinkUnderlineStyle), name: UIAccessibility.buttonShapesEnabledStatusDidChangeNotification, object: nil)
underlineTextLinksCancellable =
Preferences.shared.$underlineTextLinks
.sink { [unowned self] in
self.updateLinkUnderlineStyle(preference: $0)
}
}
@objc private func _updateLinkUnderlineStyle() {
updateLinkUnderlineStyle()
}
private func updateLinkUnderlineStyle(preference: Bool = Preferences.shared.underlineTextLinks) {
if UIAccessibility.buttonShapesEnabled || preference {
linkTextAttributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
} else {
linkTextAttributes.removeValue(forKey: .underlineStyle)
}
}
// MARK: - Emojis

View File

@ -10,7 +10,7 @@
// https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 2023.8
CURRENT_PROJECT_VERSION = 105
CURRENT_PROJECT_VERSION = 104
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev