Compare commits

..

No commits in common. "cced930549591f6ddd922e6f0c6880ab914e8293" and "fb5581ae670aef9d6260f4f8c4456ebf27bb28c4" have entirely different histories.

209 changed files with 3209 additions and 5334 deletions

View File

@ -1,38 +1,5 @@
# Changelog
## 2024.1 (115)
Features/Improvements:
- Rewrite attachment gallery
- Fixes a number of long-standing issues
- Adds a custom video player that shows controls and caption
- Supports sharing/saving videos
Bugfixes:
- Fix hang when sharing video/gifv attachments
- Fix stretched icon for Save to Photos action when sharing attachment
- Fix crash when Compose screen is dismissed while adding attachments
- Fix crash when sharing attachment from context menu on iPad
## 2024.1 (113)
Features/Improvements:
- Add Share and Save to Photos context menu actions to attachments
- Show verified link in account lists
- Change cell separator appearance on posts
Bugfixes:
- Fix tapping Followers button on profiles opening Following screen
- Fix crash when removing poll option on Compose screen
- Fix leading indentation in post text being ignored
- Fix crash when viewing posts containing HTML numeric character references
- Fix paragraphs starting with links being combined with previous paragraph
## 2024.1 (112)
Bugfixes:
- Fix profile field links not displaying
- Fix various issues displaying rich text in posts
- Fix issue changing scope after searching
- Fix crash when searching for "from:me"
## 2024.1 (111)
This build contains a complete rewrite of the HTML parsing pipeline for displaying posts. If you notice any issues with how post text appears—especially when it differs from on the web—please report it!

View File

@ -8,7 +8,6 @@
import UIKit
import MobileCoreServices
import UniformTypeIdentifiers
class ActionViewController: UIViewController {
@ -18,29 +17,25 @@ class ActionViewController: UIViewController {
super.viewDidLoad()
findURLFromWebPage { (components) in
DispatchQueue.main.async {
if let components {
self.searchForURLInApp(components)
} else {
self.findURLItem { (components) in
if let components {
DispatchQueue.main.async {
self.searchForURLInApp(components)
}
}
if let components = components {
self.searchForURLInApp(components)
} else {
self.findURLItem { (components) in
if let components = components {
self.searchForURLInApp(components)
}
}
}
}
}
private func findURLFromWebPage(completion: @escaping @Sendable (URLComponents?) -> Void) {
private func findURLFromWebPage(completion: @escaping (URLComponents?) -> Void) {
for item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else {
guard provider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) else {
continue
}
provider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil) { (result, error) in
provider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil) { (result, error) in
guard let result = result as? [String: Any],
let jsResult = result[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any],
let urlString = jsResult["activityPubURL"] as? String ?? jsResult["url"] as? String,
@ -58,13 +53,13 @@ class ActionViewController: UIViewController {
completion(nil)
}
private func findURLItem(completion: @escaping @Sendable (URLComponents?) -> Void) {
private func findURLItem(completion: @escaping (URLComponents?) -> Void) {
for item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) else {
guard provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) else {
continue
}
provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { (result, error) in
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { (result, error) in
guard let result = result as? URL,
let components = URLComponents(url: result, resolvingAgainstBaseURL: false) else {
completion(nil)

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -15,8 +15,7 @@ public protocol ComposeMastodonContext {
var instanceFeatures: InstanceFeatures { get }
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?)
func getCustomEmojis() async -> [Emoji]
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void)
@MainActor
func searchCachedAccounts(query: String) -> [AccountProtocol]

View File

@ -136,7 +136,7 @@ class AttachmentRowController: ViewController {
.overlay {
thumbnailFocusedOverlay
}
.frame(width: thumbnailSize, height: thumbnailSize)
.frame(width: 80, height: 80)
.onTapGesture {
textEditorFocused = false
// if we just focus the attachment immediately, the text editor doesn't actually unfocus
@ -162,7 +162,7 @@ class AttachmentRowController: ViewController {
switch controller.descriptionMode {
case .allowEntry:
InlineAttachmentDescriptionView(attachment: attachment, minHeight: thumbnailSize)
InlineAttachmentDescriptionView(attachment: attachment, minHeight: 80)
.matchedGeometrySource(id: AttachmentDescriptionTextViewID(attachment), presentationID: attachment.id)
.focused($textEditorFocused)
@ -177,27 +177,11 @@ class AttachmentRowController: ViewController {
Text(error.localizedDescription)
}
.onAppear(perform: controller.updateAttachmentDescriptionState)
#if os(visionOS)
.onChange(of: textEditorFocused) {
if !textEditorFocused && controller.focusAttachmentOnTextEditorUnfocus {
controller.focusAttachment()
}
}
#else
.onChange(of: textEditorFocused) { newValue in
if !newValue && controller.focusAttachmentOnTextEditorUnfocus {
controller.focusAttachment()
}
}
#endif
}
private var thumbnailSize: CGFloat {
#if os(visionOS)
120
#else
80
#endif
}
@ViewBuilder
@ -224,7 +208,6 @@ extension AttachmentRowController {
private extension View {
@available(iOS, obsoleted: 16.0)
@available(visionOS 1.0, *)
@ViewBuilder
func contextMenu<M: View, P: View>(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View {
if #available(iOS 16.0, *) {

View File

@ -40,13 +40,9 @@ class AttachmentThumbnailController: ViewController {
case .video, .gifv:
let asset = AVURLAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
#if os(visionOS)
#warning("Use async AVAssetImageGenerator.image(at:)")
#else
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
self.image = UIImage(cgImage: cgImage)
}
#endif
case .audio, .unknown:
break
@ -91,13 +87,9 @@ class AttachmentThumbnailController: ViewController {
if type.conforms(to: .movie) {
let asset = AVURLAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
#if os(visionOS)
#warning("Use async AVAssetImageGenerator.image(at:)")
#else
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
self.image = UIImage(cgImage: cgImage)
}
#endif
} else if let data = try? Data(contentsOf: url) {
if type == .gif {
self.gifController = GIFController(gifData: data)

View File

@ -85,11 +85,8 @@ class AttachmentsListController: ViewController {
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
provider.loadObject(ofClass: DraftAttachment.self) { object, error in
guard let attachment = object as? DraftAttachment else { return }
DispatchQueue.main.async { [weak self] in
guard let self,
self.canAddAttachment else {
return
}
DispatchQueue.main.async {
guard self.canAddAttachment else { return }
DraftsPersistentContainer.shared.viewContext.insert(attachment)
attachment.draft = self.draft
self.draft.attachments.add(attachment)
@ -134,9 +131,9 @@ class AttachmentsListController: ViewController {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
attachmentsList
Group {
attachmentsList
if controller.parent.config.presentAssetPicker != nil {
addImageButton
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
@ -150,10 +147,6 @@ class AttachmentsListController: ViewController {
togglePollButton
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
}
#if os(visionOS)
.buttonStyle(.bordered)
.labelStyle(AttachmentButtonLabelStyle())
#endif
}
private var attachmentsList: some View {
@ -253,11 +246,3 @@ fileprivate struct SheetOrPopover<V: View>: ViewModifier {
}
}
}
@available(visionOS 1.0, *)
fileprivate struct AttachmentButtonLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
DefaultLabelStyle().makeBody(configuration: configuration)
.foregroundStyle(.white)
}
}

View File

@ -44,7 +44,11 @@ class AutocompleteEmojisController: ViewController {
@MainActor
private func queryChanged(_ query: String) async {
var emojis = await composeController.mastodonController.getCustomEmojis()
var emojis = await withCheckedContinuation { continuation in
composeController.mastodonController.getCustomEmojis {
continuation.resume(returning: $0)
}
}
guard !Task.isCancelled else {
return
}

View File

@ -275,9 +275,7 @@ public final class ComposeController: ViewController {
@OptionalObservedObject var poster: PostService?
@EnvironmentObject var controller: ComposeController
@EnvironmentObject var draft: Draft
#if !os(visionOS)
@StateObject private var keyboardReader = KeyboardReader()
#endif
@State private var globalFrameOutsideList = CGRect.zero
init(poster: PostService?) {
@ -320,25 +318,16 @@ public final class ComposeController: ViewController {
.transition(.move(edge: .bottom))
.animation(.default, value: controller.currentInput?.autocompleteState)
#if !os(visionOS)
ControllerView(controller: { controller.toolbarController })
#endif
}
#if !os(visionOS)
// on iPadOS15, the toolbar ends up below the keyboard's toolbar without this
.padding(.bottom, keyboardInset)
#endif
.transition(.move(edge: .bottom))
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) { cancelButton }
ToolbarItem(placement: .confirmationAction) { postButton }
#if os(visionOS)
ToolbarItem(placement: .bottomOrnament) {
ControllerView(controller: { controller.toolbarController })
}
#endif
}
.background(GeometryReader { proxy in
Color.clear
@ -430,9 +419,7 @@ public final class ComposeController: ViewController {
.listRowBackground(config.backgroundColor)
}
.listStyle(.plain)
#if !os(visionOS)
.scrollDismissesKeyboardInteractivelyIfAvailable()
#endif
.disabled(controller.isPosting)
}
@ -475,7 +462,6 @@ public final class ComposeController: ViewController {
}
}
#if !os(visionOS)
@available(iOS, obsoleted: 16.0)
private var keyboardInset: CGFloat {
if #unavailable(iOS 16.0),
@ -486,7 +472,6 @@ public final class ComposeController: ViewController {
return 0
}
}
#endif
}
}

View File

@ -34,16 +34,11 @@ class PollController: ViewController {
}
private func moveOptions(indices: IndexSet, newIndex: Int) {
// see AttachmentsListController.moveAttachments
var array = poll.pollOptions
array.move(fromOffsets: indices, toOffset: newIndex)
poll.options = NSMutableOrderedSet(array: array)
poll.options.moveObjects(at: indices, to: newIndex)
}
private func removeOption(_ option: PollOption) {
var array = poll.pollOptions
array.remove(at: poll.options.index(of: option))
poll.options = NSMutableOrderedSet(array: array)
poll.options.remove(option)
}
private var canAddOption: Bool {
@ -128,15 +123,9 @@ class PollController: ViewController {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(backgroundColor)
)
#if os(visionOS)
.onChange(of: controller.duration) {
poll.duration = controller.duration.timeInterval
}
#else
.onChange(of: controller.duration) { newValue in
poll.duration = newValue.timeInterval
}
#endif
}
private var backgroundColor: Color {

View File

@ -45,26 +45,61 @@ class ToolbarController: ViewController {
@EnvironmentObject private var composeController: ComposeController
@ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22
#if !os(visionOS)
@State private var minWidth: CGFloat?
@State private var realWidth: CGFloat?
#endif
var body: some View {
#if os(visionOS)
buttons
#else
ScrollView(.horizontal, showsIndicators: false) {
buttons
.padding(.horizontal, 16)
.frame(minWidth: minWidth)
.background(GeometryReader { proxy in
Color.clear
.preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(ToolbarWidthPrefKey.self) { width in
realWidth = width
}
})
HStack(spacing: 0) {
cwButton
MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly)
#if !targetEnvironment(macCatalyst)
// the button has a bunch of extra space by default, but combined with what we add it's too much
.padding(.horizontal, -8)
#endif
.disabled(draft.editedStatusID != nil)
.disabled(composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility && draft.localOnly)
if composeController.mastodonController.instanceFeatures.localOnlyPosts {
localOnlyPicker
#if targetEnvironment(macCatalyst)
.padding(.leading, 4)
#else
.padding(.horizontal, -8)
#endif
.disabled(draft.editedStatusID != nil)
}
if let currentInput = composeController.currentInput,
currentInput.toolbarElements.contains(.emojiPicker) {
customEmojiButton
}
if let currentInput = composeController.currentInput,
currentInput.toolbarElements.contains(.formattingButtons),
composeController.config.contentType != .plain {
Spacer()
formatButtons
}
Spacer()
if #available(iOS 16.0, *),
composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection)
}
}
.padding(.horizontal, 16)
.frame(minWidth: minWidth)
.background(GeometryReader { proxy in
Color.clear
.preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(ToolbarWidthPrefKey.self) { width in
realWidth = width
}
})
}
.scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0)
.frame(height: ToolbarController.height)
@ -81,52 +116,6 @@ class ToolbarController: ViewController {
minWidth = width
}
})
#endif
}
@ViewBuilder
private var buttons: some View {
HStack(spacing: 0) {
cwButton
MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly)
#if !targetEnvironment(macCatalyst) && !os(visionOS)
// the button has a bunch of extra space by default, but combined with what we add it's too much
.padding(.horizontal, -8)
#endif
.disabled(draft.editedStatusID != nil)
.disabled(composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility && draft.localOnly)
if composeController.mastodonController.instanceFeatures.localOnlyPosts {
localOnlyPicker
#if targetEnvironment(macCatalyst)
.padding(.leading, 4)
#elseif !os(visionOS)
.padding(.horizontal, -8)
#endif
.disabled(draft.editedStatusID != nil)
}
if let currentInput = composeController.currentInput,
currentInput.toolbarElements.contains(.emojiPicker) {
customEmojiButton
}
if let currentInput = composeController.currentInput,
currentInput.toolbarElements.contains(.formattingButtons),
composeController.config.contentType != .plain {
Spacer()
formatButtons
}
Spacer()
if #available(iOS 16.0, *),
composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection)
}
}
}
private var cwButton: some View {

View File

@ -5,8 +5,6 @@
// Created by Shadowfacts on 3/7/23.
//
#if !os(visionOS)
import UIKit
import Combine
@ -39,5 +37,3 @@ class KeyboardReader: ObservableObject {
}
}
}
#endif

View File

@ -11,7 +11,7 @@ import PencilKit
extension PKDrawing {
func imageInLightMode(from rect: CGRect, scale: CGFloat = 1) -> UIImage {
func imageInLightMode(from rect: CGRect, scale: CGFloat = UIScreen.main.scale) -> UIImage {
let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light)
var drawingImage: UIImage!
lightTraitCollection.performAsCurrent {

View File

@ -8,11 +8,6 @@
import SwiftUI
extension View {
#if os(visionOS)
func scrollDisabledIfAvailable(_ disabled: Bool) -> some View {
self.scrollDisabled(disabled)
}
#else
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDisabledIfAvailable(_ disabled: Bool) -> some View {
@ -22,5 +17,4 @@ extension View {
self
}
}
#endif
}

View File

@ -22,21 +22,13 @@ struct InlineAttachmentDescriptionView: View {
self.minHeight = minHeight
}
private var placeholderOffset: CGSize {
#if os(visionOS)
CGSize(width: 8, height: 8)
#else
CGSize(width: 4, height: 8)
#endif
}
var body: some View {
ZStack(alignment: .topLeading) {
if attachment.attachmentDescription.isEmpty {
placeholder
.font(.body)
.foregroundColor(.secondary)
.offset(placeholderOffset)
.offset(x: 4, y: 8)
}
WrappedTextView(
@ -92,10 +84,6 @@ private struct WrappedTextView: UIViewRepresentable {
view.font = .preferredFont(forTextStyle: .body)
view.adjustsFontForContentSizeCategory = true
view.textContainer.lineBreakMode = .byWordWrapping
#if os(visionOS)
view.borderStyle = .roundedRect
view.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4)
#endif
return view
}

View File

@ -52,19 +52,12 @@ struct EmojiTextField: UIViewRepresentable {
if text != uiView.text {
uiView.text = text
}
if placeholder != uiView.attributedPlaceholder?.string {
uiView.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [
.foregroundColor: UIColor.secondaryLabel,
])
}
context.coordinator.text = $text
context.coordinator.maxLength = maxLength
context.coordinator.focusNextView = focusNextView
#if !os(visionOS)
uiView.backgroundColor = colorScheme == .dark ? UIColor(controller.config.fillColor) : .secondarySystemBackground
#endif
if becomeFirstResponder?.wrappedValue == true {
DispatchQueue.main.async {

View File

@ -129,9 +129,7 @@ private struct LanguagePickerList: View {
.scrollContentBackground(.hidden)
.background(groupedBackgroundColor.edgesIgnoringSafeArea(.all))
.searchable(text: $query)
#if !os(visionOS)
.scrollDismissesKeyboard(.interactively)
#endif
.navigationTitle("Post Language")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@ -154,23 +152,13 @@ private struct LanguagePickerList: View {
.map { Lang(code: $0) }
.sorted { $0.name < $1.name }
}
#if os(visionOS)
.onChange(of: query, initial: true) {
filteredLangsChanged(query: query)
}
#else
.onChange(of: query) { newValue in
filteredLangsChanged(query: newValue)
}
#endif
}
private func filteredLangsChanged(query: String) {
if query.isEmpty {
filteredLangs = nil
} else {
filteredLangs = langs.filter {
$0.name.localizedCaseInsensitiveContains(query) || $0.code.identifier.localizedCaseInsensitiveContains(query)
if newValue.isEmpty {
filteredLangs = nil
} else {
filteredLangs = langs.filter {
$0.name.localizedCaseInsensitiveContains(newValue) || $0.code.identifier.localizedCaseInsensitiveContains(newValue)
}
}
}
}

View File

@ -23,41 +23,19 @@ struct MainTextView: View {
controller.config
}
private var placeholderOffset: CGSize {
#if os(visionOS)
CGSize(width: 8, height: 8)
#else
CGSize(width: 4, height: 8)
#endif
}
private var textViewBackgroundColor: UIColor? {
#if os(visionOS)
nil
#else
colorScheme == .dark ? UIColor(config.fillColor) : .secondarySystemBackground
#endif
}
var body: some View {
ZStack(alignment: .topLeading) {
MainWrappedTextViewRepresentable(
text: $draft.text,
backgroundColor: textViewBackgroundColor,
becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder,
updateSelection: $updateSelection,
textDidChange: textDidChange
)
colorScheme == .dark ? config.fillColor : Color(uiColor: .secondarySystemBackground)
if draft.text.isEmpty {
ControllerView(controller: { PlaceholderController() })
.font(.system(size: fontSize))
.foregroundColor(.secondary)
.offset(placeholderOffset)
.offset(x: 4, y: 8)
.accessibilityHidden(true)
.allowsHitTesting(false)
}
MainWrappedTextViewRepresentable(text: $draft.text, becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder, updateSelection: $updateSelection, textDidChange: textDidChange)
}
.frame(height: effectiveHeight)
.onAppear(perform: becomeFirstResponderOnFirstAppearance)
@ -84,7 +62,6 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
typealias UIViewType = UITextView
@Binding var text: String
let backgroundColor: UIColor?
@Binding var becomeFirstResponder: Bool
@Binding var updateSelection: ((UITextView) -> Void)?
let textDidChange: (UITextView) -> Void
@ -97,16 +74,10 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
context.coordinator.textView = textView
textView.delegate = context.coordinator
textView.isEditable = true
textView.backgroundColor = .clear
textView.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 20))
textView.adjustsFontForContentSizeCategory = true
textView.textContainer.lineBreakMode = .byWordWrapping
#if os(visionOS)
textView.borderStyle = .roundedRect
// yes, the X inset is 4 less than the placeholder offset
textView.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4)
#endif
return textView
}
@ -119,8 +90,6 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
uiView.isEditable = isEnabled
uiView.keyboardType = controller.config.useTwitterKeyboard ? .twitter : .default
uiView.backgroundColor = backgroundColor
context.coordinator.text = $text
if let updateSelection {

View File

@ -7,7 +7,6 @@
import UIKit
@MainActor
public protocol DuckableViewController: UIViewController {
func duckableViewControllerShouldDuck() -> DuckAttemptAction

View File

@ -62,9 +62,7 @@ public class DuckableContainerViewController: UIViewController {
guard case .idle = state else {
if animated,
case .ducked(_, placeholder: let placeholder) = state {
#if !os(visionOS)
UIImpactFeedbackGenerator(style: .light).impactOccurred()
#endif
let origConstant = placeholder.topConstraint.constant
UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {

View File

@ -1,8 +0,0 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@ -1,26 +0,0 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "GalleryVC",
platforms: [
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "GalleryVC",
targets: ["GalleryVC"]),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "GalleryVC"),
.testTarget(
name: "GalleryVCTests",
dependencies: ["GalleryVC"]),
]
)

View File

@ -1,46 +0,0 @@
//
// GalleryContentViewController.swift
// GalleryVC
//
// Created by Shadowfacts on 3/17/24.
//
import UIKit
@MainActor
public protocol GalleryContentViewController: UIViewController {
var container: GalleryContentViewControllerContainer? { get set }
var contentSize: CGSize { get }
var activityItemsForSharing: [Any] { get }
var caption: String? { get }
var contentOverlayAccessoryViewController: UIViewController? { get }
var bottomControlsAccessoryViewController: UIViewController? { get }
var canAnimateFromSourceView: Bool { get }
func setControlsVisible(_ visible: Bool, animated: Bool)
func galleryContentDidAppear()
func galleryContentWillDisappear()
}
public extension GalleryContentViewController {
var contentOverlayAccessoryViewController: UIViewController? {
nil
}
var bottomControlsAccessoryViewController: UIViewController? {
nil
}
var canAnimateFromSourceView: Bool {
true
}
func setControlsVisible(_ visible: Bool, animated: Bool) {
}
func galleryContentDidAppear() {
}
func galleryContentWillDisappear() {
}
}

View File

@ -1,18 +0,0 @@
//
// GalleryContentViewControllerContainer.swift
// GalleryVC
//
// Created by Shadowfacts on 12/28/23.
//
import Foundation
@MainActor
public protocol GalleryContentViewControllerContainer {
var galleryControlsVisible: Bool { get }
func setGalleryContentLoading(_ loading: Bool)
func galleryContentChanged()
func disableGalleryScrollAndZoom()
func setGalleryControlsVisible(_ visible: Bool, animated: Bool)
}

View File

@ -1,22 +0,0 @@
//
// GalleryDataSource.swift
// GalleryVC
//
// Created by Shadowfacts on 12/28/23.
//
import UIKit
@MainActor
public protocol GalleryDataSource {
func galleryItemsCount() -> Int
func galleryContentViewController(forItemAt index: Int) -> GalleryContentViewController
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView?
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]?
}
public extension GalleryDataSource {
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {
nil
}
}

View File

@ -1,128 +0,0 @@
//
// GalleryDismissAnimationController.swift
// GalleryVC
//
// Created by Shadowfacts on 3/1/24.
//
import UIKit
class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
private let sourceView: UIView
private let interactiveTranslation: CGPoint?
private let interactiveVelocity: CGPoint?
init(sourceView: UIView, interactiveTranslation: CGPoint?, interactiveVelocity: CGPoint?) {
self.sourceView = sourceView
self.interactiveTranslation = interactiveTranslation
self.interactiveVelocity = interactiveVelocity
}
func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) {
guard let to = transitionContext.viewController(forKey: .to),
let from = transitionContext.viewController(forKey: .from) as? GalleryViewController else {
fatalError()
}
let itemViewController = from.currentItemViewController
if !itemViewController.content.canAnimateFromSourceView || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) {
animateCrossFadeTransition(using: transitionContext)
return
}
let container = transitionContext.containerView
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
let origSourceTransform = sourceView.transform
let appliedSourceToDestTransform: Bool
if destFrameInContainer.width > 0 && destFrameInContainer.height > 0 {
appliedSourceToDestTransform = true
let scale = min(destFrameInContainer.width / sourceFrameInContainer.width, destFrameInContainer.height / sourceFrameInContainer.height)
let sourceToDestTransform = origSourceTransform
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
.scaledBy(x: scale, y: scale)
sourceView.transform = sourceToDestTransform
} else {
appliedSourceToDestTransform = false
}
let content = itemViewController.takeContent()
content.view.translatesAutoresizingMaskIntoConstraints = true
content.view.layer.masksToBounds = true
container.addSubview(to.view)
container.addSubview(from.view)
container.addSubview(content.view)
content.view.frame = destFrameInContainer
content.view.layer.opacity = 1
container.layoutIfNeeded()
let duration = self.transitionDuration(using: transitionContext)
var initialVelocity: CGVector
if let interactiveVelocity,
let interactiveTranslation,
// very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the springs initial undershoot
sqrt(pow(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100,
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
let yDistance = sourceFrameInContainer.midY - destFrameInContainer.midY
initialVelocity = CGVector(
dx: xDistance == 0 ? 0 : interactiveVelocity.x / xDistance,
dy: yDistance == 0 ? 0 : interactiveVelocity.y / yDistance
)
} else {
initialVelocity = .zero
}
initialVelocity.dx = max(-10, min(10, initialVelocity.dx))
initialVelocity.dy = max(-10, min(10, initialVelocity.dy))
// no bounce for the dismiss animation
let spring = UISpringTimingParameters(mass: 1, stiffness: 439, damping: 42, initialVelocity: initialVelocity)
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
animator.addAnimations {
from.view.layer.opacity = 0
if appliedSourceToDestTransform {
self.sourceView.transform = origSourceTransform
}
content.view.frame = sourceFrameInContainer
content.view.layer.opacity = 0
itemViewController.setControlsVisible(false, animated: false)
}
animator.addCompletion { _ in
transitionContext.completeTransition(true)
}
animator.startAnimation()
}
private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) else {
return
}
transitionContext.containerView.addSubview(toVC.view)
transitionContext.containerView.addSubview(fromVC.view)
let duration = transitionDuration(using: transitionContext)
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
animator.addAnimations {
fromVC.view.alpha = 0
}
animator.addCompletion { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
animator.startAnimation()
}
}

View File

@ -1,83 +0,0 @@
//
// GalleryDismissInteraction.swift
// GalleryVC
//
// Created by Shadowfacts on 3/1/24.
//
import UIKit
@MainActor
class GalleryDismissInteraction: NSObject {
private let viewController: GalleryViewController
private var content: GalleryContentViewController?
private var origContentFrameInGallery: CGRect?
private var origControlsVisible: Bool?
private(set) var isActive = false
private(set) var dismissVelocity: CGPoint?
private(set) var dismissTranslation: CGPoint?
init(viewController: GalleryViewController) {
self.viewController = viewController
super.init()
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
panRecognizer.delegate = self
panRecognizer.allowedScrollTypesMask = .continuous
viewController.view.addGestureRecognizer(panRecognizer)
}
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
isActive = true
origContentFrameInGallery = viewController.view.convert(viewController.currentItemViewController.content.view.bounds, from: viewController.currentItemViewController.content.view)
content = viewController.currentItemViewController.takeContent()
content!.view.translatesAutoresizingMaskIntoConstraints = true
content!.view.frame = origContentFrameInGallery!
viewController.view.addSubview(content!.view)
origControlsVisible = viewController.currentItemViewController.controlsVisible
if origControlsVisible! {
viewController.currentItemViewController.setControlsVisible(false, animated: true)
}
case .changed:
let translation = recognizer.translation(in: viewController.view)
content!.view.frame = origContentFrameInGallery!.offsetBy(dx: translation.x, dy: translation.y)
case .ended:
let translation = recognizer.translation(in: viewController.view)
let velocity = recognizer.velocity(in: viewController.view)
dismissVelocity = velocity
dismissTranslation = translation
viewController.dismiss(animated: true)
// don't unset this until after dismiss is called, so that the dismiss animation controller can read it
isActive = false
default:
break
}
}
}
extension GalleryDismissInteraction: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let itemVC = viewController.currentItemViewController
if viewController.galleryDataSource.galleryContentTransitionSourceView(forItemAt: itemVC.itemIndex) == nil {
return false
} else if itemVC.scrollView.zoomScale > itemVC.scrollView.minimumZoomScale {
return false
} else if !itemVC.scrollAndZoomEnabled {
return false
} else {
return true
}
}
}

View File

@ -1,557 +0,0 @@
//
// GalleryItemViewController.swift
// GalleryVC
//
// Created by Shadowfacts on 12/28/23.
//
import UIKit
import AVFoundation
@MainActor
protocol GalleryItemViewControllerDelegate: AnyObject {
func isGalleryBeingPresented() -> Bool
func addPresentationAnimationCompletion(_ block: @escaping () -> Void)
func galleryItemClose(_ item: GalleryItemViewController)
func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]?
}
class GalleryItemViewController: UIViewController {
private weak var delegate: GalleryItemViewControllerDelegate?
let itemIndex: Int
let content: GalleryContentViewController
private var overlayVC: UIViewController?
private var activityIndicator: UIActivityIndicatorView?
private(set) var scrollView: UIScrollView!
private var topControlsView: UIView!
private var shareButton: UIButton!
private var shareButtonLeadingConstraint: NSLayoutConstraint!
private var shareButtonTopConstraint: NSLayoutConstraint!
private var closeButtonTrailingConstraint: NSLayoutConstraint!
private var closeButtonTopConstraint: NSLayoutConstraint!
private var bottomControlsView: UIStackView!
private(set) var captionTextView: UITextView!
private var singleTap: UITapGestureRecognizer!
private var doubleTap: UITapGestureRecognizer!
private var contentViewLeadingConstraint: NSLayoutConstraint?
private var contentViewTopConstraint: NSLayoutConstraint?
private(set) var controlsVisible: Bool = true
private(set) var scrollAndZoomEnabled = true
override var prefersHomeIndicatorAutoHidden: Bool {
return !controlsVisible
}
init(delegate: GalleryItemViewControllerDelegate, itemIndex: Int, content: GalleryContentViewController) {
self.delegate = delegate
self.itemIndex = itemIndex
self.content = content
super.init(nibName: nil, bundle: nil)
content.container = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.delegate = self
view.addSubview(scrollView)
addContent()
centerContent()
overlayVC = content.contentOverlayAccessoryViewController
if let overlayVC {
overlayVC.view.isHidden = activityIndicator != nil
overlayVC.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(overlayVC.view)
NSLayoutConstraint.activate([
overlayVC.view.leadingAnchor.constraint(equalTo: content.view.leadingAnchor),
overlayVC.view.trailingAnchor.constraint(equalTo: content.view.trailingAnchor),
overlayVC.view.topAnchor.constraint(equalTo: content.view.topAnchor),
overlayVC.view.bottomAnchor.constraint(equalTo: content.view.bottomAnchor),
])
}
topControlsView = UIView()
topControlsView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(topControlsView)
var shareConfig = UIButton.Configuration.gray()
shareConfig.cornerStyle = .dynamic
shareConfig.background.backgroundColor = .black.withAlphaComponent(0.25)
shareConfig.baseForegroundColor = .white
shareConfig.image = UIImage(systemName: "square.and.arrow.up")
shareConfig.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
shareButton = UIButton(configuration: shareConfig)
shareButton.addTarget(self, action: #selector(shareButtonPressed), for: .touchUpInside)
shareButton.isPointerInteractionEnabled = true
shareButton.pointerStyleProvider = { button, effect, shape in
return UIPointerStyle(effect: .highlight(effect.preview), shape: .roundedRect(button.frame))
}
shareButton.preferredBehavioralStyle = .pad
shareButton.translatesAutoresizingMaskIntoConstraints = false
updateShareButton()
topControlsView.addSubview(shareButton)
var closeConfig = UIButton.Configuration.gray()
closeConfig.cornerStyle = .dynamic
closeConfig.background.backgroundColor = .black.withAlphaComponent(0.25)
closeConfig.baseForegroundColor = .white
closeConfig.image = UIImage(systemName: "xmark")
closeConfig.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4)
let closeButton = UIButton(configuration: closeConfig)
closeButton.addTarget(self, action: #selector(closeButtonPressed), for: .touchUpInside)
closeButton.isPointerInteractionEnabled = true
closeButton.pointerStyleProvider = { button, effect, shape in
return UIPointerStyle(effect: .highlight(effect.preview), shape: .roundedRect(button.frame))
}
closeButton.preferredBehavioralStyle = .pad
closeButton.translatesAutoresizingMaskIntoConstraints = false
topControlsView.addSubview(closeButton)
bottomControlsView = UIStackView()
bottomControlsView.translatesAutoresizingMaskIntoConstraints = false
bottomControlsView.axis = .vertical
bottomControlsView.alignment = .fill
bottomControlsView.backgroundColor = .black.withAlphaComponent(0.5)
view.addSubview(bottomControlsView)
if let controlsAccessory = content.bottomControlsAccessoryViewController {
addChild(controlsAccessory)
bottomControlsView.addArrangedSubview(controlsAccessory.view)
controlsAccessory.didMove(toParent: self)
// Make sure the controls accessory is within the safe area.
let spacer = UIView()
bottomControlsView.addArrangedSubview(spacer)
let spacerTopConstraint = spacer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
spacerTopConstraint.priority = .init(999)
spacerTopConstraint.isActive = true
}
captionTextView = UITextView()
captionTextView.backgroundColor = .clear
captionTextView.textColor = .white
captionTextView.isEditable = false
captionTextView.isSelectable = true
captionTextView.font = .preferredFont(forTextStyle: .body)
captionTextView.adjustsFontForContentSizeCategory = true
captionTextView.alwaysBounceVertical = true
updateCaptionTextView()
bottomControlsView.addArrangedSubview(captionTextView)
#if targetEnvironment(macCatalyst)
closeButtonTopConstraint = closeButton.topAnchor.constraint(equalTo: topControlsView.safeAreaLayoutGuide.topAnchor)
shareButtonTopConstraint = shareButton.topAnchor.constraint(equalTo: topControlsView.safeAreaLayoutGuide.topAnchor)
#else
closeButtonTopConstraint = closeButton.topAnchor.constraint(equalTo: topControlsView.topAnchor)
shareButtonTopConstraint = shareButton.topAnchor.constraint(equalTo: topControlsView.topAnchor)
#endif
closeButtonTrailingConstraint = topControlsView.trailingAnchor.constraint(equalTo: closeButton.trailingAnchor)
shareButtonLeadingConstraint = shareButton.leadingAnchor.constraint(equalTo: topControlsView.leadingAnchor)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
topControlsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
topControlsView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
topControlsView.topAnchor.constraint(equalTo: view.topAnchor),
shareButtonLeadingConstraint,
shareButtonTopConstraint,
shareButton.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
shareButton.widthAnchor.constraint(equalTo: shareButton.heightAnchor),
closeButtonTrailingConstraint,
closeButtonTopConstraint,
closeButton.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
closeButton.widthAnchor.constraint(equalTo: closeButton.heightAnchor),
bottomControlsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
bottomControlsView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bottomControlsView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
captionTextView.heightAnchor.constraint(equalToConstant: 150),
])
updateTopControlsInsets()
singleTap = UITapGestureRecognizer(target: self, action: #selector(viewPressed))
singleTap.delegate = self
doubleTap = UITapGestureRecognizer(target: self, action: #selector(viewDoublePressed))
doubleTap.delegate = self
doubleTap.numberOfTapsRequired = 2
// This is needed to prevent a delay between tapping a button on and the action firing on Catalyst and Designed for iPad
doubleTap.delaysTouchesEnded = false
// this requirement is needed to make sure the double tap is ever recognized
singleTap.require(toFail: doubleTap)
view.addGestureRecognizer(singleTap)
view.addGestureRecognizer(doubleTap)
}
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
updateZoomScale(resetZoom: false)
// Ensure the transform is correct if the controls are hidden
setControlsVisible(controlsVisible, animated: false)
updateTopControlsInsets()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
centerContent()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if controlsVisible && !captionTextView.isHidden {
captionTextView.flashScrollIndicators()
}
}
func takeContent() -> GalleryContentViewController {
content.willMove(toParent: nil)
content.removeFromParent()
content.view.removeFromSuperview()
return content
}
func addContent() {
content.loadViewIfNeeded()
content.setControlsVisible(controlsVisible, animated: false)
content.view.translatesAutoresizingMaskIntoConstraints = false
if content.parent != self {
addChild(content)
content.didMove(toParent: self)
}
if scrollAndZoomEnabled {
scrollView.addSubview(content.view)
contentViewLeadingConstraint = content.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
contentViewLeadingConstraint!.isActive = true
contentViewTopConstraint = content.view.topAnchor.constraint(equalTo: scrollView.topAnchor)
contentViewTopConstraint!.isActive = true
updateZoomScale(resetZoom: true)
} else {
// If the content was previously added, deactivate the old constraints.
contentViewLeadingConstraint?.isActive = false
contentViewTopConstraint?.isActive = false
view.addSubview(content.view)
NSLayoutConstraint.activate([
content.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
content.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
content.view.topAnchor.constraint(equalTo: view.topAnchor),
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
if let overlayVC {
NSLayoutConstraint.activate([
overlayVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
overlayVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
overlayVC.view.topAnchor.constraint(equalTo: view.topAnchor),
overlayVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
content.view.layoutIfNeeded()
}
func setControlsVisible(_ visible: Bool, animated: Bool) {
controlsVisible = visible
guard let topControlsView,
let bottomControlsView else {
return
}
func updateControlsViews() {
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
content.setControlsVisible(visible, animated: animated)
}
if animated {
let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters())
animator.addAnimations(updateControlsViews)
animator.startAnimation()
} else {
updateControlsViews()
}
setNeedsUpdateOfHomeIndicatorAutoHidden()
}
func updateZoomScale(resetZoom: Bool) {
guard scrollAndZoomEnabled else {
scrollView.maximumZoomScale = 1
scrollView.minimumZoomScale = 1
scrollView.zoomScale = 1
return
}
guard content.contentSize.width > 0 && content.contentSize.height > 0 else {
return
}
let heightScale = view.bounds.height / content.contentSize.height
let widthScale = view.bounds.width / content.contentSize.width
let minScale = min(widthScale, heightScale)
let maxScale = minScale >= 1 ? minScale + 2 : 2
scrollView.minimumZoomScale = minScale
scrollView.maximumZoomScale = maxScale
if resetZoom {
scrollView.zoomScale = minScale
} else {
scrollView.zoomScale = max(minScale, min(maxScale, scrollView.zoomScale))
}
centerContent()
}
private func centerContent() {
guard scrollAndZoomEnabled else {
return
}
// Note: use frame for the content.view, because that's in the coordinate space of the scroll view
// which means it's already been scaled by the zoom factor.
let yOffset = max(0, (view.bounds.height - content.view.frame.height) / 2)
contentViewTopConstraint!.constant = yOffset
let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2)
contentViewLeadingConstraint!.constant = xOffset
}
private func updateShareButton() {
shareButton.isEnabled = !content.activityItemsForSharing.isEmpty
}
private func updateCaptionTextView() {
guard let caption = content.caption,
!caption.isEmpty else {
captionTextView.isHidden = true
return
}
captionTextView.text = caption
}
private func updateTopControlsInsets() {
let notchedDeviceTopInsets: [CGFloat] = [
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
48, // iPhone XR, 11
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
50, // iPhone 12 mini, 13 mini
]
let islandDeviceTopInsets: [CGFloat] = [
59, // iPhone 14 Pro, 14 Pro Max, 15 Pro, 15 Pro Max
]
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
// the notch width is not the same for the iPhones 13,
// but what we actually want is the same offset from the edges
// since the corner radius didn't change
let notchWidth: CGFloat = 210
let earWidth = (view.bounds.width - notchWidth) / 2
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
shareButtonLeadingConstraint.constant = offset
closeButtonTrailingConstraint.constant = offset
} else if islandDeviceTopInsets.contains(view.safeAreaInsets.top) {
shareButtonLeadingConstraint.constant = 24
shareButtonTopConstraint.constant = 24
closeButtonTrailingConstraint.constant = 24
closeButtonTopConstraint.constant = 24
} else {
shareButtonLeadingConstraint.constant = 8
shareButtonTopConstraint.constant = 8
closeButtonTrailingConstraint.constant = 8
closeButtonTopConstraint.constant = 8
}
}
private func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero
zoomRect.size.width = content.view.frame.width / scale
zoomRect.size.height = content.view.frame.height / scale
let newCenter = scrollView.convert(center, to: content.view)
zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
return zoomRect
}
private func animateZoomOut() {
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
animator.addAnimations {
self.scrollView.zoomScale = self.scrollView.minimumZoomScale
self.scrollView.layoutIfNeeded()
}
animator.startAnimation()
}
// MARK: Interaction
@objc private func viewPressed() {
if scrollAndZoomEnabled,
scrollView.zoomScale > scrollView.minimumZoomScale {
animateZoomOut()
} else {
setControlsVisible(!controlsVisible, animated: true)
}
}
@objc private func viewDoublePressed(_ recognizer: UITapGestureRecognizer) {
guard scrollAndZoomEnabled else {
return
}
if scrollView.zoomScale <= scrollView.minimumZoomScale {
let point = recognizer.location(in: recognizer.view)
let scale = min(
max(
scrollView.bounds.width / content.contentSize.width,
scrollView.bounds.height / content.contentSize.height,
scrollView.zoomScale + 0.75
),
scrollView.maximumZoomScale
)
let rect = zoomRectFor(scale: scale, center: point)
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
animator.addAnimations {
self.scrollView.zoom(to: rect, animated: false)
self.view.layoutIfNeeded()
}
animator.startAnimation()
} else {
animateZoomOut()
}
}
@objc private func closeButtonPressed() {
delegate?.galleryItemClose(self)
}
@objc private func shareButtonPressed() {
let items = content.activityItemsForSharing
guard !items.isEmpty else {
return
}
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: delegate?.galleryItemApplicationActivities(self))
activityVC.popoverPresentationController?.sourceView = shareButton
present(activityVC, animated: true)
}
}
extension GalleryItemViewController: GalleryContentViewControllerContainer {
var galleryControlsVisible: Bool {
controlsVisible
}
func setGalleryContentLoading(_ loading: Bool) {
if loading {
overlayVC?.view.isHidden = true
if activityIndicator == nil {
let activityIndicator = UIActivityIndicatorView(style: .large)
self.activityIndicator = activityIndicator
activityIndicator.startAnimating()
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
} else {
if let activityIndicator {
// If we're in the middle of the presentation animation,
// wait until it finishes to hide the loading indicator.
// Since the updated content frame won't affect the animation,
// make sure the loading indicator remains visible.
if let delegate,
delegate.isGalleryBeingPresented() {
delegate.addPresentationAnimationCompletion { [unowned self] in
self.setGalleryContentLoading(false)
}
} else {
activityIndicator.removeFromSuperview()
self.activityIndicator = nil
self.overlayVC?.view.isHidden = false
}
}
}
}
func galleryContentChanged() {
updateZoomScale(resetZoom: true)
updateShareButton()
updateCaptionTextView()
}
func disableGalleryScrollAndZoom() {
scrollAndZoomEnabled = false
updateZoomScale(resetZoom: true)
scrollView.isScrollEnabled = false
// Make sure the content is re-added with the correct constraints
if content.parent == self {
addContent()
}
}
func setGalleryControlsVisible(_ visible: Bool, animated: Bool) {
setControlsVisible(visible, animated: animated)
}
}
extension GalleryItemViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
if scrollAndZoomEnabled {
return content.view
} else {
return nil
}
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
if scrollView.zoomScale <= scrollView.minimumZoomScale {
setControlsVisible(true, animated: true)
} else {
setControlsVisible(false, animated: true)
}
centerContent()
scrollView.layoutIfNeeded()
}
}
extension GalleryItemViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == singleTap {
let loc = gestureRecognizer.location(in: view)
return !topControlsView.frame.contains(loc) && !bottomControlsView.frame.contains(loc)
} else if gestureRecognizer == doubleTap {
let loc = gestureRecognizer.location(in: content.view)
return content.view.bounds.contains(loc)
} else {
return true
}
}
}

View File

@ -1,139 +0,0 @@
//
// GalleryPresentationAnimationController.swift
// GalleryVC
//
// Created by Shadowfacts on 12/28/23.
//
import UIKit
class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
private let sourceView: UIView
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
fatalError()
}
let itemViewController = to.currentItemViewController
if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions {
animateCrossFadeTransition(using: transitionContext)
return
}
let container = transitionContext.containerView
to.view.frame = container.bounds
container.addSubview(to.view)
container.layoutIfNeeded()
// Make sure the zoom scale is updated before getting the content view frame, since it needs to take into account the correct transform.
itemViewController.updateZoomScale(resetZoom: true)
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
// Use a transformation to make the actual source view appear to move into the destination frame.
// Doing this while having the content view fade-in papers over the z-index change when
// there was something overlapping the source view.
let origSourceTransform = sourceView.transform
let sourceToDestTransform: CGAffineTransform?
if destFrameInContainer.width > 0 && destFrameInContainer.height > 0 {
// Scale evenly in both dimensions, to prevent the source view appearing to stretch/distort during the animation.
let scale = min(destFrameInContainer.width / sourceFrameInContainer.width, destFrameInContainer.height / sourceFrameInContainer.height)
sourceToDestTransform = origSourceTransform
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
.scaledBy(x: scale, y: scale)
} else {
sourceToDestTransform = nil
}
let content = itemViewController.takeContent()
content.view.translatesAutoresizingMaskIntoConstraints = true
container.insertSubview(content.view, belowSubview: to.view)
// Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content.
let dimmingView = UIView()
dimmingView.backgroundColor = .black
dimmingView.frame = container.bounds
dimmingView.layer.opacity = 0
container.insertSubview(dimmingView, belowSubview: content.view)
to.view.backgroundColor = nil
to.view.layer.opacity = 0
content.view.frame = sourceFrameInContainer
content.view.layer.opacity = 0
container.layoutIfNeeded()
// This needs to take place after the layout, so that the transform is correct.
itemViewController.setControlsVisible(false, animated: false)
let duration = self.transitionDuration(using: transitionContext)
// rougly equivalent to duration: 0.35, bounce: 0.3
let spring = UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero)
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
animator.addAnimations {
dimmingView.layer.opacity = 1
to.view.layer.opacity = 1
content.view.frame = destFrameInContainer
content.view.layer.opacity = 1
itemViewController.setControlsVisible(true, animated: false)
if let sourceToDestTransform {
self.sourceView.transform = sourceToDestTransform
}
}
animator.addCompletion { _ in
dimmingView.removeFromSuperview()
to.view.backgroundColor = .black
if sourceToDestTransform != nil {
self.sourceView.transform = origSourceTransform
}
itemViewController.addContent()
transitionContext.completeTransition(true)
to.presentationAnimationCompleted()
}
animator.startAnimation()
}
private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
return
}
transitionContext.containerView.addSubview(to.view)
to.view.alpha = 0
let duration = transitionDuration(using: transitionContext)
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
animator.addAnimations {
to.view.alpha = 1
}
animator.addCompletion { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
to.presentationAnimationCompleted()
}
animator.startAnimation()
}
}

View File

@ -1,168 +0,0 @@
//
// GalleryViewController.swift
// GalleryVC
//
// Created by Shadowfacts on 12/28/23.
//
import UIKit
public class GalleryViewController: UIPageViewController {
let galleryDataSource: GalleryDataSource
let initialItemIndex: Int
private let _itemsCount: Int
private var itemsCount: Int {
get {
precondition(_itemsCount == galleryDataSource.galleryItemsCount(), "GalleryDataSource item count cannot change")
return _itemsCount
}
}
var currentItemViewController: GalleryItemViewController {
viewControllers![0] as! GalleryItemViewController
}
private var dismissInteraction: GalleryDismissInteraction!
private var presentationAnimationCompletionHandlers: [() -> Void] = []
override public var prefersStatusBarHidden: Bool {
true
}
override public var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
.none
}
override public var childForHomeIndicatorAutoHidden: UIViewController? {
currentItemViewController
}
public init(dataSource: GalleryDataSource, initialItemIndex: Int) {
self.galleryDataSource = dataSource
self.initialItemIndex = initialItemIndex
self._itemsCount = dataSource.galleryItemsCount()
precondition(initialItemIndex >= 0 && initialItemIndex < _itemsCount, "initialItemIndex is out of bounds")
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [
.interPageSpacing: 50
])
modalPresentationStyle = .fullScreen
transitioningDelegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
dismissInteraction = GalleryDismissInteraction(viewController: self)
view.backgroundColor = .black
overrideUserInterfaceStyle = .dark
dataSource = self
delegate = self
setViewControllers([makeItemVC(index: initialItemIndex)], direction: .forward, animated: false)
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
currentItemViewController.content.galleryContentWillDisappear()
}
}
private func makeItemVC(index: Int) -> GalleryItemViewController {
let content = galleryDataSource.galleryContentViewController(forItemAt: index)
return GalleryItemViewController(delegate: self, itemIndex: index, content: content)
}
func presentationAnimationCompleted() {
for block in presentationAnimationCompletionHandlers {
block()
}
currentItemViewController.content.galleryContentDidAppear()
}
}
extension GalleryViewController: UIPageViewControllerDataSource {
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewController = viewController as? GalleryItemViewController else {
preconditionFailure("VC must be GalleryItemViewController")
}
guard viewController.itemIndex > 0 else {
return nil
}
return makeItemVC(index: viewController.itemIndex - 1)
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewController = viewController as? GalleryItemViewController else {
preconditionFailure("VC must be GalleryItemViewController")
}
guard viewController.itemIndex < itemsCount - 1 else {
return nil
}
return makeItemVC(index: viewController.itemIndex + 1)
}
}
extension GalleryViewController: UIPageViewControllerDelegate {
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
currentItemViewController.content.galleryContentWillDisappear()
}
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
currentItemViewController.content.galleryContentDidAppear()
}
}
extension GalleryViewController: GalleryItemViewControllerDelegate {
func isGalleryBeingPresented() -> Bool {
isBeingPresented
}
func addPresentationAnimationCompletion(_ block: @escaping () -> Void) {
presentationAnimationCompletionHandlers.append(block)
}
func galleryItemClose(_ item: GalleryItemViewController) {
dismiss(animated: true)
}
func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]? {
galleryDataSource.galleryApplicationActivities(forItemAt: item.itemIndex)
}
}
extension GalleryViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: initialItemIndex) {
return GalleryPresentationAnimationController(sourceView: sourceView)
} else {
return nil
}
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: currentItemViewController.itemIndex) {
let translation: CGPoint?
let velocity: CGPoint?
if let dismissInteraction,
dismissInteraction.isActive {
translation = dismissInteraction.dismissTranslation
velocity = dismissInteraction.dismissVelocity
} else {
translation = nil
velocity = nil
}
return GalleryDismissAnimationController(sourceView: sourceView, interactiveTranslation: translation, interactiveVelocity: velocity)
} else {
return nil
}
}
}

View File

@ -1,12 +0,0 @@
import XCTest
@testable import GalleryVC
final class GalleryVCTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest
// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}

View File

@ -10,7 +10,7 @@ import Foundation
import Combine
import Pachyderm
public final class InstanceFeatures: ObservableObject {
public class InstanceFeatures: ObservableObject {
private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; (pleroma|akkoma) (.*)\\)", options: .caseInsensitive)
private let _featuresUpdated = PassthroughSubject<Void, Never>()

View File

@ -225,7 +225,6 @@ class MatchedGeometryDismissAnimationController<Content: View>: NSObject, UIView
animator.addCompletion { _ in
transitionContext.completeTransition(true)
matchedGeomVC.state.animating = false
matchedGeomVC.state.mode = .idle
}
animator.startAnimation()
}

View File

@ -12,7 +12,7 @@ import WebURL
/**
The base Mastodon API client.
*/
public struct Client: Sendable {
public class Client {
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
@ -20,6 +20,8 @@ public struct Client: Sendable {
let session: URLSession
public var accessToken: String?
public var appID: String?
public var clientID: String?
public var clientSecret: String?
@ -59,11 +61,9 @@ public struct Client: Sendable {
return encoder
}()
public init(baseURL: URL, accessToken: String? = nil, clientID: String? = nil, clientSecret: String? = nil, session: URLSession = .shared) {
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
self.baseURL = baseURL
self.accessToken = accessToken
self.clientID = clientID
self.clientSecret = clientSecret
self.session = session
}
@ -150,7 +150,14 @@ public struct Client: Sendable {
"scopes" => scopes.scopeString,
"website" => website?.absoluteString
]))
run(request, completion: completion)
run(request) { result in
defer { completion(result) }
guard case let .success(application, _) = result else { return }
self.appID = application.id
self.clientID = application.clientID
self.clientSecret = application.clientSecret
}
}
public func getAccessToken(authorizationCode: String, redirectURI: String, scopes: [Scope], completion: @escaping Callback<LoginSettings>) {
@ -162,7 +169,12 @@ public struct Client: Sendable {
"redirect_uri" => redirectURI,
"scope" => scopes.scopeString,
]))
run(request, completion: completion)
run(request) { result in
defer { completion(result) }
guard case let .success(loginSettings, _) = result else { return }
self.accessToken = loginSettings.accessToken
}
}
public func revokeAccessToken() async throws {
@ -186,16 +198,21 @@ public struct Client: Sendable {
})
}
public func nodeInfo() async throws -> NodeInfo {
public func nodeInfo(completion: @escaping Callback<NodeInfo>) {
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
let wellKnownResults = try await run(wellKnown).0
if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
let href = WebURL(url.href),
href.host == WebURL(self.baseURL)?.host {
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
return try await run(nodeInfo).0
} else {
throw NodeInfoError.noWellKnownLink
run(wellKnown) { result in
switch result {
case let .failure(error):
completion(.failure(error))
case let .success(wellKnown, _):
if let url = wellKnown.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
let href = WebURL(url.href),
href.host == WebURL(self.baseURL)?.host {
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
self.run(nodeInfo, completion: completion)
}
}
}
}
@ -583,15 +600,4 @@ extension Client {
case invalidModel(Swift.Error)
case mastodonError(Int, String)
}
enum NodeInfoError: LocalizedError {
case noWellKnownLink
var errorDescription: String? {
switch self {
case .noWellKnownLink:
return "No well-known link"
}
}
}
}

View File

@ -7,7 +7,7 @@
import Foundation
public enum SearchOperatorType: String, CaseIterable, Equatable, Sendable {
public enum SearchOperatorType: String, CaseIterable, Equatable {
case has
case `is`
case language

View File

@ -7,7 +7,7 @@
import Foundation
public struct StatusEdit: Decodable, Sendable {
public struct StatusEdit: Decodable {
public let content: String
public let spoilerText: String
public let sensitive: Bool
@ -28,10 +28,10 @@ public struct StatusEdit: Decodable, Sendable {
case emojis
}
public struct Poll: Decodable, Sendable {
public struct Poll: Decodable {
public let options: [Option]
public struct Option: Decodable, Sendable {
public struct Option: Decodable {
public let title: String
}
}

View File

@ -7,7 +7,7 @@
import Foundation
public struct StatusSource: Decodable, Sendable {
public struct StatusSource: Decodable {
public let id: String
public let text: String
public let spoilerText: String

View File

@ -49,7 +49,7 @@ public class InstanceSelector {
}
public extension InstanceSelector {
struct Instance: Codable, Sendable {
struct Instance: Codable {
public let domain: String
public let description: String
public let proxiedThumbnailURL: URL

View File

@ -8,7 +8,7 @@
import Foundation
import Pachyderm
public enum PostVisibility: Codable, Hashable, CaseIterable, Sendable {
public enum PostVisibility: Codable, Hashable, CaseIterable {
case serverDefault
case visibility(Visibility)
@ -59,7 +59,6 @@ public enum ReplyVisibility: Codable, Hashable, CaseIterable {
public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
@MainActor
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
switch self {
case .sameAsPost:

View File

@ -12,14 +12,12 @@ import Combine
public final class Preferences: Codable, ObservableObject {
@MainActor
public static var shared: Preferences = load()
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
@MainActor
public static func save() {
let encoder = PropertyListEncoder()
let data = try? encoder.encode(shared)
@ -35,7 +33,6 @@ public final class Preferences: Codable, ObservableObject {
return Preferences()
}
@MainActor
public static func migrate(from url: URL) -> Result<Void, any Error> {
do {
try? FileManager.default.removeItem(at: archiveURL)

View File

@ -9,7 +9,7 @@
import UIKit
import Pachyderm
public enum StatusSwipeAction: String, Codable, Hashable, CaseIterable, Sendable {
public enum StatusSwipeAction: String, Codable, Hashable, CaseIterable {
case reply
case favorite
case reblog

View File

@ -8,7 +8,7 @@
import Foundation
import CryptoKit
public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
public struct UserAccountInfo: Equatable, Hashable, Identifiable {
public let id: String
public let instanceURL: URL
public let clientID: String

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
</dict>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>54BD.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
</dict>
</array>
</dict>
</plist>

View File

@ -19,11 +19,7 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
let image = UIImage(data: data) else {
return nil
}
#if os(visionOS)
let size: CGFloat = 50 * 2
#else
let size = 50 * UIScreen.main.scale
#endif
return await image.byPreparingThumbnail(ofSize: CGSize(width: size, height: size)) ?? image
}
@ -132,7 +128,7 @@ extension UIColor {
return .systemBackground
}
}
static let appGroupedBackground = UIColor { traitCollection in
if case .dark = traitCollection.userInterfaceStyle,
!Preferences.shared.pureBlackDarkMode {

View File

@ -12,7 +12,7 @@ import UserAccounts
import InstanceFeatures
import Combine
final class ShareMastodonContext: ComposeMastodonContext, ObservableObject, Sendable {
class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
let accountInfo: UserAccountInfo?
let client: Client
let instanceFeatures: InstanceFeatures
@ -20,9 +20,7 @@ final class ShareMastodonContext: ComposeMastodonContext, ObservableObject, Send
@MainActor
private var customEmojis: [Emoji]?
@MainActor
@Published
private(set) var ownAccount: Account?
@Published var ownAccount: Account?
init(accountInfo: UserAccountInfo) {
self.accountInfo = accountInfo
@ -31,7 +29,16 @@ final class ShareMastodonContext: ComposeMastodonContext, ObservableObject, Send
Task { @MainActor in
async let instance = try? await run(Client.getInstanceV1()).0
async let nodeInfo = try? await client.nodeInfo()
async let nodeInfo: NodeInfo? = await withCheckedContinuation({ continuation in
self.client.nodeInfo { response in
switch response {
case .success(let nodeInfo, _):
continuation.resume(returning: nodeInfo)
case .failure(_):
continuation.resume(returning: nil)
}
}
})
guard let instance = await instance else { return }
self.instanceFeatures.update(instance: InstanceInfo(v1: instance), nodeInfo: await nodeInfo)
}
@ -59,13 +66,15 @@ final class ShareMastodonContext: ComposeMastodonContext, ObservableObject, Send
}
@MainActor
func getCustomEmojis() async -> [Emoji] {
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
if let customEmojis {
return customEmojis
completion(customEmojis)
} else {
let emojis = (try? await self.run(Client.getCustomEmoji()).0) ?? []
self.customEmojis = emojis
return emojis
Task.detached { @MainActor in
let emojis = (try? await self.run(Client.getCustomEmoji()).0) ?? []
self.customEmojis = emojis
completion(emojis)
}
}
}

View File

@ -54,7 +54,6 @@ struct SwitchAccountContainerView: View {
}
}
@MainActor
private struct AccountButtonLabel: View {
static let urlSession = URLSession(configuration: .ephemeral)

View File

@ -7,11 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */; };
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D14BAE22B34A2800642648 /* GalleryViewController.swift */; };
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
@ -83,8 +85,10 @@
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; };
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */; };
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; };
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; };
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; };
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; };
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; };
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
@ -98,7 +102,7 @@
D635237129B78A7D009ED5E7 /* TuskerComponents in Frameworks */ = {isa = PBXBuildFile; productRef = D635237029B78A7D009ED5E7 /* TuskerComponents */; };
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D63CC701290EC0B8000E19DE /* Sentry */; };
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = D63CC701290EC0B8000E19DE /* Sentry */; };
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */; };
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */; };
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */; };
@ -109,7 +113,9 @@
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */; };
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; };
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
D642E83A2BA75F4C004BFD6A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D642E8392BA75F4C004BFD6A /* PrivacyInfo.xcprivacy */; };
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */; };
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */; };
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD12A06F2510059ECEB /* NotificationsCollectionViewController.swift */; };
@ -118,15 +124,17 @@
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; };
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvPlayerView.swift */; };
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6552366289870790048A653 /* ScreenCorners */; };
D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; productRef = D6552366289870790048A653 /* ScreenCorners */; };
D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; };
D659F36229541065002D944A /* TTTView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D659F36129541065002D944A /* TTTView.swift */; };
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B532971F71D00DABDFB /* EditedReport.swift */; };
@ -146,10 +154,12 @@
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; };
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */; };
@ -157,6 +167,7 @@
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; };
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
@ -179,27 +190,13 @@
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.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 */; };
D691296E2BA75ADF005C58ED /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D691296D2BA75ACF005C58ED /* PrivacyInfo.xcprivacy */; };
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */; };
D691771529A6FCAB0054D7EF /* StateRestorableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */; };
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; };
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; };
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; };
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */; };
D69261272BB3BA610023152C /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261262BB3BA610023152C /* Box.swift */; };
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; };
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; };
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */; };
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */; };
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */; };
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; };
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; };
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */; };
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */; };
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; };
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; };
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; };
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; };
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
@ -259,15 +256,13 @@
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 */; };
D6BC74842AFC3DF9000DD603 /* TrendingLinkCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC74832AFC3DF9000DD603 /* TrendingLinkCardView.swift */; };
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.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 */; };
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
D6BD395929B64426005FFD2B /* ComposeUI in Frameworks */ = {isa = PBXBuildFile; productRef = D6BD395829B64426005FFD2B /* ComposeUI */; };
D6BD395B29B64441005FFD2B /* ComposeHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */; };
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6BEA244291A0EDE002F4D01 /* Duckable */; };
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 */; };
@ -282,8 +277,10 @@
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */; };
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; };
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D6CA6ED129EF6091003EC5DF /* TuskerPreferences */; };
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; };
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */; };
@ -310,13 +307,13 @@
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; };
D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; };
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F582A13293200AB2315 /* BackgroundManager.swift */; };
D6D79F5B2A13D22B00AB2315 /* GalleryFallbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F5A2A13D22B00AB2315 /* GalleryFallbackViewController.swift */; };
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
D6DD8FFD298495A8002AD3FD /* LogoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFC298495A8002AD3FD /* LogoutService.swift */; };
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */; };
D6DD996B2998611A0015C962 /* SuggestedProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */; };
D6DEBA8D2B6579830008629A /* MainThreadBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DEBA8C2B6579830008629A /* MainThreadBox.swift */; };
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* Weak.swift */; };
@ -324,7 +321,7 @@
D6E343AB265AAD6B00C4AA01 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AA265AAD6B00C4AA01 /* Media.xcassets */; };
D6E343AD265AAD6B00C4AA01 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E343AC265AAD6B00C4AA01 /* ActionViewController.swift */; };
D6E343B0265AAD6B00C4AA01 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6E343AE265AAD6B00C4AA01 /* MainInterface.storyboard */; };
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D6E343BA265AAD8C00C4AA01 /* Action.js in Resources */ = {isa = PBXBuildFile; fileRef = D6E343B9265AAD8C00C4AA01 /* Action.js */; };
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */; };
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
@ -410,11 +407,13 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingLargeImageViewController.swift; sourceTree = "<group>"; };
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = "<group>"; };
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
04D14BAE22B34A2800642648 /* GalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewController.swift; sourceTree = "<group>"; };
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
@ -485,8 +484,10 @@
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = "<group>"; };
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableViewCell.swift; sourceTree = "<group>"; };
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = "<group>"; };
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = "<group>"; };
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = "<group>"; };
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = "<group>"; };
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = "<group>"; };
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
@ -510,8 +511,9 @@
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderView.xib; sourceTree = "<group>"; };
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
D642E8392BA75F4C004BFD6A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
D642E83D2BA7AD0F004BFD6A /* GalleryVC */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = GalleryVC; sourceTree = "<group>"; };
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldValueView.swift; sourceTree = "<group>"; };
D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldVerificationView.swift; sourceTree = "<group>"; };
D646DCD12A06F2510059ECEB /* NotificationsCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsCollectionViewController.swift; sourceTree = "<group>"; };
@ -520,13 +522,15 @@
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvPlayerView.swift; sourceTree = "<group>"; };
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; };
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = "<group>"; };
D65B4B532971F71D00DABDFB /* EditedReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedReport.swift; sourceTree = "<group>"; };
@ -549,11 +553,13 @@
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = "<group>"; };
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = "<group>"; };
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Pachyderm; path = Packages/Pachyderm; sourceTree = "<group>"; };
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherIndicatorView.swift; sourceTree = "<group>"; };
@ -561,6 +567,7 @@
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = "<group>"; };
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
@ -576,7 +583,7 @@
D68A76EB295369A8001DA1B3 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsView.swift; sourceTree = "<group>"; };
D68A76F029539116001DA1B3 /* FlipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipView.swift; sourceTree = "<group>"; };
D68A76F22953915C001DA1B3 /* TTTKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TTTKit; sourceTree = "<group>"; };
D68A76F22953915C001DA1B3 /* TTTKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TTTKit; path = Packages/TTTKit; sourceTree = "<group>"; };
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTrendsViewController.swift; sourceTree = "<group>"; };
@ -584,26 +591,13 @@
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>"; };
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = "<group>"; };
D691296D2BA75ACF005C58ED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainActor+Unsafe.swift"; sourceTree = "<group>"; };
D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRestorableViewController.swift; sourceTree = "<group>"; };
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; };
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; };
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; };
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOverlayViewController.swift; sourceTree = "<group>"; };
D69261262BB3BA610023152C /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.swift; sourceTree = "<group>"; };
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryDataSource.swift; sourceTree = "<group>"; };
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F372BA8E2B7002B1C8D /* GifvController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvController.swift; sourceTree = "<group>"; };
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = "<group>"; };
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.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>"; };
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = "<group>"; };
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
@ -639,7 +633,7 @@
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>"; };
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MatchedGeometryPresentation; sourceTree = "<group>"; };
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MatchedGeometryPresentation; path = Packages/MatchedGeometryPresentation; sourceTree = "<group>"; };
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = "<group>"; };
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineDescriptionCollectionViewCell.swift; sourceTree = "<group>"; };
D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusCollectionViewCell.swift; sourceTree = "<group>"; };
@ -648,7 +642,7 @@
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedImageView.swift; sourceTree = "<group>"; };
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
D6B0026C29B5245400C70BE2 /* UserAccounts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = UserAccounts; sourceTree = "<group>"; };
D6B0026C29B5245400C70BE2 /* UserAccounts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = UserAccounts; path = Packages/UserAccounts; sourceTree = "<group>"; };
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; };
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
@ -662,16 +656,14 @@
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>"; };
D6BC74832AFC3DF9000DD603 /* TrendingLinkCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkCardView.swift; sourceTree = "<group>"; };
D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfileCardView.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>"; };
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
D6BD395729B6441F005FFD2B /* ComposeUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ComposeUI; sourceTree = "<group>"; };
D6BD395729B6441F005FFD2B /* ComposeUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ComposeUI; path = Packages/ComposeUI; sourceTree = "<group>"; };
D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeHostingController.swift; sourceTree = "<group>"; };
D6BD395C29B789D5005FFD2B /* TuskerComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TuskerComponents; sourceTree = "<group>"; };
D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Duckable; sourceTree = "<group>"; };
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>"; };
@ -686,9 +678,11 @@
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TuskerPreferences; sourceTree = "<group>"; };
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = "<group>"; };
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerPreferences; path = Packages/TuskerPreferences; sourceTree = "<group>"; };
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = "<group>"; };
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToPhotosActivity.swift; sourceTree = "<group>"; };
D6CF5B822AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSCollectionLayoutSection+Readable.swift"; sourceTree = "<group>"; };
@ -721,13 +715,13 @@
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = "<group>"; };
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
D6D79F582A13293200AB2315 /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
D6D79F5A2A13D22B00AB2315 /* GalleryFallbackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryFallbackViewController.swift; sourceTree = "<group>"; };
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutService.swift; sourceTree = "<group>"; };
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewController.swift; sourceTree = "<group>"; };
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedProfilesViewController.swift; sourceTree = "<group>"; };
D6DEBA8C2B6579830008629A /* MainThreadBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadBox.swift; sourceTree = "<group>"; };
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
D6DFC69F242C4CCC00ACC392 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
@ -763,7 +757,7 @@
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteListService.swift; sourceTree = "<group>"; };
D6F6A556291F4F1600F496A8 /* MuteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteAccountView.swift; sourceTree = "<group>"; };
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
D6FA94DF29B52891006AAC51 /* InstanceFeatures */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = InstanceFeatures; sourceTree = "<group>"; };
D6FA94DF29B52891006AAC51 /* InstanceFeatures */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = InstanceFeatures; path = Packages/InstanceFeatures; sourceTree = "<group>"; };
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -792,7 +786,6 @@
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */,
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */,
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */,
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
@ -826,20 +819,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0411610522B457290030A9B7 /* Gallery */ = {
0411610522B457290030A9B7 /* Attachment Gallery */ = {
isa = PBXGroup;
children = (
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */,
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */,
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */,
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */,
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */,
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */,
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */,
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */,
04D14BAE22B34A2800642648 /* GalleryViewController.swift */,
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */,
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */,
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */,
);
path = Gallery;
path = "Attachment Gallery";
sourceTree = "<group>";
};
D60089172981FEA4005B4D00 /* Tip Jar */ = {
@ -916,10 +904,9 @@
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
D6E77D0A286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift */,
D6E77D0C286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib */,
D6BC74832AFC3DF9000DD603 /* TrendingLinkCardView.swift */,
D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */,
D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */,
D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */,
D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */,
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
@ -999,13 +986,14 @@
children = (
D65B4B89297879DE00DABDFB /* Account Follows */,
D6A3BC822321F69400FD64D5 /* Account List */,
0411610522B457290030A9B7 /* Attachment Gallery */,
D641C787213DD862004B4513 /* Compose */,
D641C785213DD83B004B4513 /* Conversation */,
D6F2E960249E772F005846BB /* Crash Reporter */,
D61F759729384D4200C0B37F /* Customize Timelines */,
D627943C23A5635D00D38C68 /* Explore */,
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
0411610522B457290030A9B7 /* Gallery */,
D641C788213DD86D004B4513 /* Large Image */,
D627944B23A9A02400D38C68 /* Lists */,
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */,
D641C782213DD7F0004B4513 /* Main */,
@ -1108,6 +1096,19 @@
path = Compose;
sourceTree = "<group>";
};
D641C788213DD86D004B4513 /* Large Image */ = {
isa = PBXGroup;
children = (
D646C954213B364600269FB5 /* Transitions */,
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */,
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
D681A299249AD62D0085E54E /* LargeImageContentView.swift */,
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */,
D6D79F5A2A13D22B00AB2315 /* GalleryFallbackViewController.swift */,
);
path = "Large Image";
sourceTree = "<group>";
};
D641C789213DD87E004B4513 /* Preferences */ = {
isa = PBXGroup;
children = (
@ -1159,21 +1160,14 @@
path = "Profile Header";
sourceTree = "<group>";
};
D642E83C2BA7ACC3004BFD6A /* Packages */ = {
D646C954213B364600269FB5 /* Transitions */ = {
isa = PBXGroup;
children = (
D674A50727F910F300BA03AC /* Pachyderm */,
D6BEA243291A0C83002F4D01 /* Duckable */,
D68A76F22953915C001DA1B3 /* TTTKit */,
D6B0026C29B5245400C70BE2 /* UserAccounts */,
D6FA94DF29B52891006AAC51 /* InstanceFeatures */,
D6BD395C29B789D5005FFD2B /* TuskerComponents */,
D6BD395729B6441F005FFD2B /* ComposeUI */,
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */,
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */,
D642E83D2BA7AD0F004BFD6A /* GalleryVC */,
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */,
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */,
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */,
);
path = Packages;
path = Transitions;
sourceTree = "<group>";
};
D64AAE8F26C80DB600FC57FB /* Toast */ = {
@ -1230,6 +1224,7 @@
D667E5F62135C2ED0057A976 /* Extensions */ = {
isa = PBXGroup;
children = (
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */,
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */,
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */,
@ -1241,6 +1236,7 @@
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */,
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */,
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */,
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */,
D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */,
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
@ -1318,7 +1314,6 @@
D6A4533129F0CFCA00032932 /* SwitchAccountContainerView.swift */,
D6A4531729EF64BA00032932 /* MainInterface.storyboard */,
D6A4531A29EF64BA00032932 /* Info.plist */,
D642E8392BA75F4C004BFD6A /* PrivacyInfo.xcprivacy */,
);
path = ShareExtension;
sourceTree = "<group>";
@ -1355,8 +1350,6 @@
children = (
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */,
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */,
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */,
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */,
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */,
@ -1401,6 +1394,7 @@
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */,
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */,
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
D6A3BC872321F78000FD64D5 /* Account Cell */,
D67C57A721E2649B00C3118B /* Account Detail */,
D6C7D27B22B6EBE200071952 /* Attachments */,
@ -1447,8 +1441,7 @@
isa = PBXGroup;
children = (
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */,
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */,
D6934F372BA8E2B7002B1C8D /* GifvController.swift */,
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */,
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */,
);
path = Attachments;
@ -1460,7 +1453,15 @@
D63CC703290EC472000E19DE /* Dist.xcconfig */,
D6D706A829498C82000827ED /* Tusker.xcconfig */,
D6B5F3BC2ACA586C00309734 /* Version.xcconfig */,
D642E83C2BA7ACC3004BFD6A /* Packages */,
D674A50727F910F300BA03AC /* Pachyderm */,
D6BEA243291A0C83002F4D01 /* Duckable */,
D68A76F22953915C001DA1B3 /* TTTKit */,
D6B0026C29B5245400C70BE2 /* UserAccounts */,
D6FA94DF29B52891006AAC51 /* InstanceFeatures */,
D6BD395C29B789D5005FFD2B /* TuskerComponents */,
D6BD395729B6441F005FFD2B /* ComposeUI */,
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */,
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */,
D6D4DDCE212518A000E1C4BB /* Tusker */,
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
@ -1490,12 +1491,10 @@
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
D6D4DDDB212518A200E1C4BB /* Info.plist */,
D60088F02980D938005B4D00 /* Tusker.storekit */,
D691296D2BA75ACF005C58ED /* PrivacyInfo.xcprivacy */,
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */,
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
D6D79F582A13293200AB2315 /* BackgroundManager.swift */,
D69261262BB3BA610023152C /* Box.swift */,
D61F75B6293C119700C0B37F /* Filterer.swift */,
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
D61F75BA293C183100C0B37F /* HTMLConverter.swift */,
@ -1503,7 +1502,6 @@
D61F75BC293D099600C0B37F /* Lazy.swift */,
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D61DC84528F498F200B82C6E /* Logging.swift */,
D6DEBA8C2B6579830008629A /* MainThreadBox.swift */,
D6B81F432560390300F6E31D /* MenuController.swift */,
D6CF5B842AC7C56F00F15D83 /* MultiColumnCollectionViewLayout.swift */,
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
@ -1710,7 +1708,6 @@
D6BD395829B64426005FFD2B /* ComposeUI */,
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
D6934F2B2BA7AD32002B1C8D /* GalleryVC */,
);
productName = Tusker;
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
@ -1844,7 +1841,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D642E83A2BA75F4C004BFD6A /* PrivacyInfo.xcprivacy in Resources */,
D6A4531929EF64BA00032932 /* MainInterface.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1856,12 +1852,13 @@
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
D691296E2BA75ADF005C58ED /* PrivacyInfo.xcprivacy in Resources */,
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */,
D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */,
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */,
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
@ -1955,7 +1952,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */,
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */,
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */,
@ -1984,9 +1981,7 @@
D65B4B682977769E00DABDFB /* StatusActionAccountListViewController.swift in Sources */,
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */,
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */,
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */,
@ -1995,17 +1990,15 @@
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */,
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */,
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */,
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */,
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */,
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */,
@ -2014,12 +2007,12 @@
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */,
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */,
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */,
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */,
D6D94955298963A900C59229 /* Colors.swift in Sources */,
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
@ -2037,23 +2030,23 @@
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */,
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */,
D61F75AD293AF39000C0B37F /* Filter+Helpers.swift in Sources */,
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */,
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */,
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */,
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */,
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */,
@ -2075,6 +2068,7 @@
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */,
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
@ -2083,7 +2077,6 @@
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */,
D6BC74842AFC3DF9000DD603 /* TrendingLinkCardView.swift in Sources */,
D65B4B5E2973040D00DABDFB /* ReportAddStatusView.swift in Sources */,
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
@ -2118,6 +2111,7 @@
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */,
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */,
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
@ -2145,6 +2139,7 @@
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */,
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 */,
@ -2153,7 +2148,6 @@
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */,
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */,
D6DEBA8D2B6579830008629A /* MainThreadBox.swift in Sources */,
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
@ -2163,8 +2157,6 @@
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */,
@ -2173,10 +2165,10 @@
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */,
D69261272BB3BA610023152C /* Box.swift in Sources */,
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
D6D79F5B2A13D22B00AB2315 /* GalleryFallbackViewController.swift in Sources */,
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */,
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
@ -2186,12 +2178,14 @@
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
D61ABEF828EFC3F900B29151 /* ProfileStatusesViewController.swift in Sources */,
D6ADB6EA28E91C30009924AB /* TimelineStatusCollectionViewCell.swift in Sources */,
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
@ -2205,8 +2199,10 @@
D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */,
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
@ -2219,11 +2215,11 @@
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */,
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
@ -2235,7 +2231,6 @@
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */,
D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */,
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */,
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
@ -2300,6 +2295,7 @@
};
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */;
targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */;
};
@ -2421,11 +2417,10 @@
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6,7";
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Dist;
};
@ -2489,10 +2484,9 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6,7";
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Dist;
};
@ -2517,11 +2511,10 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@ -2546,11 +2539,10 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
@ -2575,11 +2567,10 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Dist;
};
@ -2729,13 +2720,12 @@
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6,7";
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Debug;
};
@ -2760,11 +2750,10 @@
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Tusker/Tusker-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6,7";
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Release;
};
@ -2869,10 +2858,9 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6,7";
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Debug;
};
@ -2895,10 +2883,9 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
SUPPORTS_MACCATALYST = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6,7";
TARGETED_DEVICE_FAMILY = "1,2,6";
};
name = Release;
};
@ -2972,8 +2959,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git";
requirement = {
kind = exactVersion;
version = 0.2.3;
kind = upToNextMinorVersion;
minimumVersion = 0.1.0;
};
};
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
@ -2981,7 +2968,7 @@
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 8.21.0;
minimumVersion = 8.15.0;
};
};
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = {
@ -3039,10 +3026,6 @@
isa = XCSwiftPackageProductDependency;
productName = Pachyderm;
};
D6934F2B2BA7AD32002B1C8D /* GalleryVC */ = {
isa = XCSwiftPackageProductDependency;
productName = GalleryVC;
};
D6A4532329EF665200032932 /* ComposeUI */ = {
isa = XCSwiftPackageProductDependency;
productName = ComposeUI;

View File

@ -29,11 +29,9 @@ class FavoriteService {
status.favourited.toggle()
mastodonController.persistentContainer.statusSubject.send(status.id)
#if !os(visionOS)
if hapticFeedback {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
#endif
let request = (status.favourited ? Status.favourite : Status.unfavourite)(status.id)
do {
@ -51,11 +49,9 @@ class FavoriteService {
}
presenter.showToast(configuration: config, animated: true)
#if !os(visionOS)
if hapticFeedback {
UINotificationFeedbackGenerator().notificationOccurred(.error)
}
#endif
}
}

View File

@ -11,20 +11,18 @@ import Pachyderm
import Combine
import UserAccounts
import InstanceFeatures
#if canImport(Sentry)
import Sentry
#endif
import ComposeUI
import OSLog
private let oauthScopes = [Scope.read, .write, .follow]
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MastodonController")
final class MastodonController: ObservableObject, Sendable {
class MastodonController: ObservableObject {
@MainActor
static private(set) var all = [UserAccountInfo: MastodonController]()
@available(*, message: "do something less dumb")
static var first: MastodonController { all.first!.value }
@MainActor
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
if let controller = all[account] {
@ -36,12 +34,10 @@ final class MastodonController: ObservableObject, Sendable {
}
}
@MainActor
static func removeForAccount(_ account: UserAccountInfo) {
all.removeValue(forKey: account)
}
@MainActor
static func resetAll() {
all = [:]
}
@ -50,31 +46,26 @@ final class MastodonController: ObservableObject, Sendable {
nonisolated let persistentContainer: MastodonCachePersistentStore// = MastodonCachePersistentStore(for: accountInfo, transient: transient)
let instanceURL: URL
let accountInfo: UserAccountInfo?
@MainActor
private(set) var accountPreferences: AccountPreferences!
var accountInfo: UserAccountInfo?
var accountPreferences: AccountPreferences!
private(set) var client: Client!
let client: Client!
let instanceFeatures = InstanceFeatures()
@MainActor @Published private(set) var account: AccountMO?
@MainActor @Published private(set) var instance: InstanceV1?
@MainActor @Published private var instanceV2: InstanceV2?
@MainActor @Published private(set) var instanceInfo: InstanceInfo!
@MainActor @Published private(set) var nodeInfo: NodeInfo!
@MainActor @Published private(set) var lists: [List] = []
@MainActor @Published private(set) var customEmojis: [Emoji]?
@MainActor @Published private(set) var followedHashtags: [FollowedHashtag] = []
@MainActor @Published private(set) var filters: [FilterMO] = []
@Published private(set) var account: AccountMO?
@Published private(set) var instance: InstanceV1?
@Published private var instanceV2: InstanceV2?
@Published private(set) var instanceInfo: InstanceInfo!
@Published private(set) var nodeInfo: NodeInfo!
@Published private(set) var lists: [List] = []
@Published private(set) var customEmojis: [Emoji]?
@Published private(set) var followedHashtags: [FollowedHashtag] = []
@Published private(set) var filters: [FilterMO] = []
private var cancellables = Set<AnyCancellable>()
@MainActor
private var fetchOwnInstanceTask: Task<InstanceV1, any Error>?
@MainActor
private var fetchOwnAccountTask: Task<MainThreadBox<AccountMO>, any Error>?
@MainActor
private var fetchCustomEmojisTask: Task<[Emoji], Never>?
private var pendingOwnInstanceRequestCallbacks = [(Result<InstanceV1, Client.Error>) -> Void]()
private var ownInstanceRequest: URLSessionTask?
var loggedIn: Bool {
accountInfo != nil
@ -106,7 +97,6 @@ final class MastodonController: ObservableObject, Sendable {
}
.store(in: &cancellables)
#if canImport(Sentry)
$instanceInfo
.compactMap { $0 }
.removeDuplicates(by: { $0.version == $1.version })
@ -115,7 +105,6 @@ final class MastodonController: ObservableObject, Sendable {
setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo)
}
.store(in: &cancellables)
#endif
$instance
.compactMap { $0 }
@ -229,37 +218,22 @@ final class MastodonController: ObservableObject, Sendable {
Task {
do {
_ = try await getOwnAccount()
} catch {
logger.error("Fetch own account failed: \(String(describing: error))")
}
}
let instanceTask = Task {
do {
_ = try await getOwnInstance()
async let ownAccount = try getOwnAccount()
async let ownInstance = try getOwnInstance()
_ = try await (ownAccount, ownInstance)
if instanceFeatures.hasMastodonVersion(4, 0, 0) {
_ = try? await getOwnInstanceV2()
async let _ = try? getOwnInstanceV2()
}
loadLists()
_ = await loadFilters()
await loadServerPreferences()
} catch {
logger.error("Fetch instance failed: \(String(describing: error))")
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
}
}
Task {
_ = await instanceTask.value
await loadLists()
}
Task {
_ = await instanceTask.value
_ = await loadFilters()
}
Task {
_ = await instanceTask.value
await loadServerPreferences()
}
}
@MainActor
@ -272,93 +246,131 @@ final class MastodonController: ObservableObject, Sendable {
}
}
@MainActor
func getOwnAccount() async throws -> AccountMO {
func getOwnAccount(completion: ((Result<AccountMO, Client.Error>) -> Void)? = nil) {
if let account {
return account
} else if let fetchOwnAccountTask {
return try await fetchOwnAccountTask.value.value
completion?(.success(account))
} else {
let task = Task {
let account = try await run(Client.getSelfAccount()).0
let context = persistentContainer.viewContext
// this closure is declared separately so we can tell the compiler it's Sendable
let performBlock: @MainActor @Sendable () -> MainThreadBox<AccountMO> = {
let accountMO: AccountMO
if let existing = self.persistentContainer.account(for: account.id, in: context) {
accountMO = existing
existing.updateFrom(apiAccount: account, container: self.persistentContainer)
} else {
accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context)
let request = Client.getSelfAccount()
run(request) { response in
switch response {
case let .failure(error):
completion?(.failure(error))
case let .success(account, _):
let context = self.persistentContainer.viewContext
context.perform {
let accountMO: AccountMO
if let existing = self.persistentContainer.account(for: account.id, in: context) {
accountMO = existing
existing.updateFrom(apiAccount: account, container: self.persistentContainer)
} else {
accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context)
}
accountMO.active = true
self.account = accountMO
completion?(.success(accountMO))
}
// TODO: is AccountMO.active used anywhere?
accountMO.active = true
self.account = accountMO
return MainThreadBox(value: accountMO)
}
// it's safe to remove the MainActor annotation, because this is the view context
return await context.perform(unsafeBitCast(performBlock, to: (@Sendable () -> MainThreadBox<AccountMO>).self))
}
fetchOwnAccountTask = task
return try await task.value.value
}
}
func getOwnInstance(completion: (@Sendable (InstanceV1) -> Void)? = nil) {
Task.detached {
let ownInstance = try? await self.getOwnInstance()
if let ownInstance {
completion?(ownInstance)
func getOwnAccount() async throws -> AccountMO {
if let account = account {
return account
} else {
return try await withCheckedThrowingContinuation({ continuation in
self.getOwnAccount { result in
continuation.resume(with: result)
}
})
}
}
func getOwnInstance(completion: ((InstanceV1) -> Void)? = nil) {
getOwnInstanceInternal(retryAttempt: 0) {
if case let .success(instance) = $0 {
completion?(instance)
}
}
}
@MainActor
func getOwnInstance() async throws -> InstanceV1 {
if let instance {
return instance
} else if let fetchOwnInstanceTask {
return try await fetchOwnInstanceTask.value
return try await withCheckedThrowingContinuation({ continuation in
getOwnInstanceInternal(retryAttempt: 0) { result in
continuation.resume(with: result)
}
})
}
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<InstanceV1, Client.Error>) -> Void)?) {
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
assert(Thread.isMainThread)
if let instance = self.instance {
completion?(.success(instance))
} else {
let task = Task {
let instance = try await retrying("Fetch Own Instance") {
try await run(Client.getInstanceV1()).0
}
self.instance = instance
Task {
await fetchNodeInfo()
if let completion = completion {
pendingOwnInstanceRequestCallbacks.append(completion)
}
if ownInstanceRequest == nil {
let request = Client.getInstanceV1()
ownInstanceRequest = run(request) { (response) in
switch response {
case .failure(let error):
let delay: DispatchTimeInterval
switch retryAttempt {
case 0:
delay = .seconds(1)
case 1:
delay = .seconds(5)
case 2:
delay = .seconds(30)
case 3:
delay = .seconds(60)
default:
// if we've failed four times, just give up :/
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(.failure(error))
}
self.pendingOwnInstanceRequestCallbacks = []
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
// completion is nil because in this invocation of getOwnInstanceInternal we've already added it to the pending callbacks array
self.getOwnInstanceInternal(retryAttempt: retryAttempt + 1, completion: nil)
}
case let .success(instance, _):
DispatchQueue.main.async {
self.ownInstanceRequest = nil
self.instance = instance
for completion in self.pendingOwnInstanceRequestCallbacks {
completion(.success(instance))
}
self.pendingOwnInstanceRequestCallbacks = []
}
}
}
return instance
}
fetchOwnInstanceTask = task
return try await task.value
}
}
@MainActor
private func fetchNodeInfo() async {
if let nodeInfo = try? await client.nodeInfo() {
self.nodeInfo = nodeInfo
}
}
private func retrying<T: Sendable>(_ label: StaticString, action: @Sendable () async throws -> T) async throws -> T {
for attempt in 0..<4 {
do {
return try await action()
} catch {
let seconds = UInt64(truncating: pow(2, attempt) as NSNumber)
logger.error("\(label, privacy: .public) failed, waiting \(seconds, privacy: .public)s before retrying. Reason: \(String(describing: error))")
try! await Task.sleep(nanoseconds: seconds * NSEC_PER_SEC)
client.nodeInfo { result in
switch result {
case let .failure(error):
print("Unable to get node info: \(error)")
case let .success(nodeInfo, _):
DispatchQueue.main.async {
self.nodeInfo = nodeInfo
}
}
}
}
}
return try await action()
}
@MainActor
private func getOwnInstanceV2() async throws {
self.instanceV2 = try await client.run(Client.getInstanceV2()).0
}
@ -409,43 +421,43 @@ final class MastodonController: ObservableObject, Sendable {
}
}
@MainActor
func getCustomEmojis() async -> [Emoji] {
if let customEmojis {
return customEmojis
} else if let fetchCustomEmojisTask {
return await fetchCustomEmojisTask.value
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
if let emojis = self.customEmojis {
completion(emojis)
} else {
let task = Task {
let emojis = (try? await run(Client.getCustomEmoji()).0) ?? []
customEmojis = emojis
return emojis
let request = Client.getCustomEmoji()
run(request) { (response) in
if case let .success(emojis, _) = response {
DispatchQueue.main.async {
self.customEmojis = emojis
}
completion(emojis)
} else {
completion([])
}
}
fetchCustomEmojisTask = task
return await task.value
}
}
private func loadLists() async {
private func loadLists() {
let req = Client.getLists()
guard let (lists, _) = try? await run(req) else {
return
}
let sorted = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
await MainActor.run {
self.lists = sorted
}
let context = persistentContainer.backgroundContext
await context.perform {
for list in lists {
if let existing = try? context.fetch(ListMO.fetchRequest(id: list.id)).first {
existing.updateFrom(apiList: list)
} else {
_ = ListMO(apiList: list, context: context)
run(req) { response in
if case .success(let lists, _) = response {
DispatchQueue.main.async {
self.lists = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
}
let context = self.persistentContainer.backgroundContext
context.perform {
for list in lists {
if let existing = try? context.fetch(ListMO.fetchRequest(id: list.id)).first {
existing.updateFrom(apiList: list)
} else {
_ = ListMO(apiList: list, context: context)
}
}
self.persistentContainer.save(context: context)
}
}
self.persistentContainer.save(context: context)
}
}
@ -532,7 +544,6 @@ final class MastodonController: ObservableObject, Sendable {
filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? []
}
@MainActor
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft {
var acctsToMention = [String]()
@ -585,7 +596,6 @@ final class MastodonController: ObservableObject, Sendable {
)
}
@MainActor
func createDraft(editing status: StatusMO, source: StatusSource) -> Draft {
precondition(status.id == source.id)
let draft = DraftsPersistentContainer.shared.createEditDraft(
@ -602,7 +612,6 @@ final class MastodonController: ObservableObject, Sendable {
}
#if canImport(Sentry)
private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?) {
let crumb = Breadcrumb(level: .info, category: "MastodonController")
crumb.data = [
@ -618,4 +627,3 @@ private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?)
}
SentrySDK.addBreadcrumb(crumb)
}
#endif

View File

@ -80,11 +80,9 @@ class ReblogService {
status.reblogged.toggle()
mastodonController.persistentContainer.statusSubject.send(status.id)
#if !os(visionOS)
if hapticFeedback {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
#endif
let request: Request<Status>
if status.reblogged {
@ -106,11 +104,9 @@ class ReblogService {
}
presenter.showToast(configuration: config, animated: true)
#if !os(visionOS)
if hapticFeedback {
UINotificationFeedbackGenerator().notificationOccurred(.error)
}
#endif
}
}

View File

@ -1,44 +0,0 @@
//
// ImageActivityItemSource.swift
// Tusker
//
// Created by Shadowfacts on 3/19/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import UniformTypeIdentifiers
class ImageActivityItemSource: NSObject, UIActivityItemSource {
let data: Data
let url: URL
let image: UIImage?
init(data: Data, url: URL, image: UIImage?) {
self.data = data
self.url = url
self.image = image
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return url
}
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
return image
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
do {
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
try data.write(to: tempURL)
return tempURL
} catch {
return nil
}
}
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
return (UTType(filenameExtension: url.pathExtension) ?? .image).identifier
}
}

View File

@ -23,14 +23,7 @@ class SaveToPhotosActivity: UIActivity {
return "Save to Photos"
}
override var activityImage: UIImage? {
// Just using the symbol image directly causes it to be stretched.
let symbol = UIImage(systemName: "square.and.arrow.down", withConfiguration: UIImage.SymbolConfiguration(scale: .large))!
let format = UIGraphicsImageRendererFormat()
format.scale = UIScreen.main.scale
return UIGraphicsImageRenderer(size: CGSize(width: 76, height: 76), format: format).image { ctx in
let rect = AVMakeRect(aspectRatio: symbol.size, insideRect: CGRect(x: 0, y: 0, width: 76, height: 76))
symbol.draw(in: rect)
}
UIImage(systemName: "square.and.arrow.down")
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {

View File

@ -33,7 +33,7 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
let metadata = LPLinkMetadata()
metadata.originalURL = status.url!
metadata.url = status.url!
let converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
let content = converter.convert(html: status.content)
metadata.title = "\(status.account.displayName): \"\(content)\""
if let avatar = status.account.avatar,

View File

@ -1,44 +0,0 @@
//
// VideoActivityItemSource.swift
// Tusker
//
// Created by Shadowfacts on 3/19/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import UniformTypeIdentifiers
import AVFoundation
class VideoActivityItemSource: UIActivityItemProvider {
private let asset: AVAsset
private let url: URL
private var tempURL: URL?
init(asset: AVAsset, url: URL) {
self.asset = asset
self.url = url
super.init(placeholderItem: url)
}
override var item: Any {
if let tempURL {
return tempURL
}
do {
let data = try Data(contentsOf: url)
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
try data.write(to: tempURL)
self.tempURL = tempURL
return tempURL
} catch {
return url
}
}
override func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
return (UTType(filenameExtension: url.pathExtension) ?? .video).identifier
}
}

View File

@ -9,9 +9,7 @@
import UIKit
import CoreData
import OSLog
#if canImport(Sentry)
import Sentry
#endif
import UserAccounts
import ComposeUI
import TuskerPreferences
@ -21,17 +19,13 @@ typealias Preferences = TuskerPreferences.Preferences
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate")
@main
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
#if canImport(Sentry)
configureSentry()
#endif
#if !os(visionOS)
swizzleStatusBar()
swizzlePresentationController()
#endif
AppShortcutItem.createItems(for: application)
@ -62,9 +56,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let oldPreferencesFile = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
if FileManager.default.fileExists(atPath: oldPreferencesFile.path) {
if case .failure(let error) = Preferences.migrate(from: oldPreferencesFile) {
#if canImport(Sentry)
SentrySDK.capture(error: error)
#endif
}
}
@ -78,9 +70,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) {
DraftsPersistentContainer.shared.migrate(from: url) {
if case .failure(let error) = $0 {
#if canImport(Sentry)
SentrySDK.capture(error: error)
#endif
}
}
}
@ -91,7 +81,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true
}
#if canImport(Sentry)
private func configureSentry() {
guard let dsn = Bundle.main.object(forInfoDictionaryKey: "SentryDSN") as? String,
!dsn.isEmpty else {
@ -123,17 +112,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
event.context?.removeValue(forKey: "culture")
return Preferences.shared.reportErrorsAutomatically ? event : nil
}
if let clazz = NSClassFromString("SentryInstallation"),
let objClazz = clazz as AnyObject as? NSObject,
let id = objClazz.perform(Selector(("idWithCacheDirectoryPath:")), with: options.cacheDirectoryPath).takeUnretainedValue() as? String {
logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)")
}
}
if let clazz = NSClassFromString("SentryInstallation"),
let objClazz = clazz as AnyObject as? NSObject,
let id = objClazz.value(forKey: "id") as? String {
logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)")
}
}
#endif
override func buildMenu(with builder: UIMenuBuilder) {
if builder.system == .main {
MenuController.buildMainMenu(builder: builder)
}
@ -173,7 +162,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
#if !os(visionOS)
private func swizzleStatusBar() {
let selector = Selector(("handleTapAction:"))
var originalIMP: IMP?
@ -225,6 +213,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Logging.general.error("Unable to swizzle presentation controller")
}
}
#endif
}

View File

@ -1,18 +0,0 @@
//
// Box.swift
// Tusker
//
// Created by Shadowfacts on 3/26/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import Foundation
@propertyWrapper
class Box<Value> {
var wrappedValue: Value
init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}

View File

@ -8,22 +8,11 @@
import UIKit
#if os(visionOS)
private let imageScale: CGFloat = 2
#else
@MainActor
private let imageScale = UIScreen.main.scale
#endif
final class ImageCache: @unchecked Sendable {
class ImageCache {
@MainActor
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24 * 7), desiredSize: CGSize(width: 50, height: 50))
@MainActor
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
@MainActor
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
@MainActor
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
#if DEBUG
@ -35,15 +24,14 @@ final class ImageCache: @unchecked Sendable {
private let cache: ImageDataCache
private let desiredPixelSize: CGSize?
@MainActor
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
let pixelSize = desiredSize?.applying(.init(scaleX: imageScale, y: imageScale))
let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
self.desiredPixelSize = pixelSize
self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize)
}
func get(_ url: URL, loadOriginal: Bool = false, completion: (@Sendable (Data?, UIImage?) -> Void)?) -> Request? {
func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
if !ImageCache.disableCaching,
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
completion?(entry.data, entry.image)
@ -53,7 +41,7 @@ final class ImageCache: @unchecked Sendable {
}
}
func getFromSource(_ url: URL, completion: (@Sendable (Data?, UIImage?) -> Void)?) -> Request? {
func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
return Task.detached(priority: .userInitiated) {
let result = await self.fetch(url: url)
switch result {

View File

@ -11,9 +11,7 @@ import CoreData
import Pachyderm
import Combine
import OSLog
#if canImport(Sentry)
import Sentry
#endif
import CloudKit
import UserAccounts
@ -201,7 +199,6 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
try context.save()
} catch let error as NSError {
logger.error("Unable to save managed object context: \(String(describing: error), privacy: .public)")
#if canImport(Sentry)
let crumb = Breadcrumb(level: .fatal, category: "PersistentStore")
// note: NSDetailedErrorsKey == "NSDetailedErrorsKey" != "NSDetailedErrors"
if let detailed = error.userInfo["NSDetailedErrors"] as? [NSError] {
@ -220,7 +217,6 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
]
}
SentrySDK.addBreadcrumb(crumb)
#endif
fatalError("Unable to save managed object context: \(String(describing: error))")
}
}
@ -550,19 +546,14 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
}
}
}
// Can't capture vars in concurrently-executing closure
let hashtags = changedHashtags
let instances = changedInstances
let timelinePositions = changedTimelinePositions
let accountPrefs = changedAccountPrefs
DispatchQueue.main.async {
if hashtags {
if changedHashtags {
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
}
if instances {
if changedInstances {
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil)
}
for id in timelinePositions {
for id in changedTimelinePositions {
guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else {
continue
}
@ -570,7 +561,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
timelinePosition.changedRemotely()
NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition)
}
if accountPrefs {
if changedAccountPrefs {
NotificationCenter.default.post(name: .accountPreferencesChangedRemotely, object: nil)
}
}

View File

@ -86,7 +86,6 @@ func fromTimelineKind(_ kind: String) -> Timeline {
// replace with Collection.trimmingPrefix
@available(iOS, obsoleted: 16.0)
@available(visionOS 1.0, *)
private func trimmingPrefix(_ prefix: String, of str: String) -> Substring {
return str[str.index(str.startIndex, offsetBy: prefix.count)...]
}

View File

@ -8,12 +8,23 @@
import Foundation
private let ASCII_NEWLINE: unichar = 10
private let ASCII_SPACE: unichar = 32
extension NSAttributedString {
var fullRange: NSRange {
return NSRange(location: 0, length: self.length)
}
/// Creates a new string with the whitespace collapsed according to the CSS Text Module Level 3 rules.
/// See https://www.w3.org/TR/css-text-3/#white-space-phase-1
func collapsingWhitespace() -> NSAttributedString {
let mut = NSMutableAttributedString(attributedString: self)
mut.collapseWhitespace()
return mut
}
}
extension NSMutableAttributedString {
@ -47,4 +58,56 @@ extension NSMutableAttributedString {
}
}
/// Collapses whitespace in this string according to the CSS Text Module Level 3 rules.
/// See https://www.w3.org/TR/css-text-3/#white-space-phase-1
func collapseWhitespace() {
let str = self.mutableString
var i = 0
while i < str.length {
if str.character(at: i) == ASCII_NEWLINE {
var j: Int
if i > 0 {
// scan backwards to find beginning of space characters preceeding newline
j = i - 1
while j >= 0 {
if str.character(at: j) != ASCII_SPACE {
break
}
j -= 1
}
// add one after loop completes because start of range is _inclusive_
j += 1
} else {
j = 0
}
var k: Int
if i < str.length - 1 {
// scan forwards to find end of space characters following newline
k = i + 1
while k < str.length {
if str.character(at: k) != ASCII_SPACE {
break
}
k += 1
}
// don't need to subtract one before breaking out of loop, because end of range is _exclusive_
} else {
// range end is _exclusive_, so use whole string length that way last character is included
k = str.length
}
// if there's only one character to be replaced, that means we'd be replacing the newline with a newline, so don't bother
if k - j > 1 {
str.replaceCharacters(in: NSRange(location: j, length: k - j), with: "\n")
// continue scanning through the string starting after the newline we just inserted
i = j
}
}
i += 1
}
}
}

View File

@ -9,7 +9,7 @@
import Foundation
/*
Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/daf69ed837fa04d4ba666f5a99378cf1815f0dab/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift
Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/fe3b3fd5436b196d8c5211ab2cc4b69fc35524fe/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift
Copyright (c) 2022, Chime
All rights reserved.
@ -46,23 +46,10 @@ public extension MainActor {
/// 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.
@_unavailableFromAsync
@MainActor(unsafe)
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
return try MainActor.assumeIsolated(body)
}
dispatchPrecondition(condition: .onQueue(.main))
return try withoutActuallyEscaping(body) { fn in
try unsafeBitCast(fn, to: (() throws -> T).self)()
}
}
@_unavailableFromAsync
@available(*, deprecated, message: "Tool of last resort, do not use this.")
static func runUnsafelyMaybeIntroducingDataRace<T>(_ body: @MainActor () throws -> T) rethrows -> T {
return try withoutActuallyEscaping(body) { fn in
try unsafeBitCast(fn, to: (() throws -> T).self)()
}
return try body()
}
}

View File

@ -0,0 +1,33 @@
//
// PKDrawing+Render.swift
// Tusker
//
// Created by Shadowfacts on 5/9/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import PencilKit
extension PKDrawing {
func imageInLightMode(from rect: CGRect, scale: CGFloat = UIScreen.main.scale) -> UIImage {
let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light)
var drawingImage: UIImage!
lightTraitCollection.performAsCurrent {
drawingImage = self.image(from: rect, scale: scale)
}
let imageRect = CGRect(origin: .zero, size: rect.size)
let format = UIGraphicsImageRendererFormat()
format.opaque = false
format.scale = scale
let renderer = UIGraphicsImageRenderer(size: rect.size, format: format)
return renderer.image { (context) in
UIColor.white.setFill()
context.fill(imageRect)
drawingImage.draw(in: imageRect)
}
}
}

View File

@ -0,0 +1,36 @@
//
// UIViewController+Delegate.swift
// Tusker
//
// Created by Shadowfacts on 8/27/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
extension UIViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let presented = presented as? LargeImageAnimatableViewController,
presented.animationImage != nil {
return LargeImageExpandAnimationController()
}
return nil
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if let dismissed = dismissed as? LargeImageAnimatableViewController,
dismissed.animationImage != nil {
return LargeImageShrinkAnimationController(interactionController: dismissed.dismissInteractionController)
}
return nil
}
public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
if let animator = animator as? LargeImageShrinkAnimationController,
let interactionController = animator.interactionController,
interactionController.inProgress {
return interactionController
}
return nil
}
}

View File

@ -10,7 +10,6 @@ import SwiftUI
import Combine
extension View {
@MainActor
@ViewBuilder
func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View {
if #available(iOS 16.0, *) {

View File

@ -23,7 +23,7 @@ class HTMLConverter {
return style
}()
private let converter: AttributedStringConverter<Callbacks>
private var converter: AttributedStringConverter<Callbacks>
init(font: UIFont, monospaceFont: UIFont, color: UIColor, paragraphStyle: NSParagraphStyle) {
let config = AttributedStringConverterConfiguration(font: font, monospaceFont: monospaceFont, color: color, paragraphStyle: paragraphStyle)

View File

@ -15,10 +15,7 @@ struct ImageGrayscalifier {
private static let cache = NSCache<NSURL, UIImage>()
static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? {
let grayscale = MainActor.runUnsafelyMaybeIntroducingDataRace {
Preferences.shared.grayscaleImages
}
if grayscale,
if Preferences.shared.grayscaleImages,
let source = image.cgImage {
// todo: should this return the original image if conversion fails?
return convert(url: url, cgImage: source)
@ -27,18 +24,6 @@ struct ImageGrayscalifier {
}
}
static func convertIfNecessary(url: URL?, image: UIImage) async -> UIImage? {
let grayscale = await MainActor.run {
Preferences.shared.grayscaleImages
}
if grayscale,
let source = image.cgImage {
return await convert(url: url, cgImage: source)
} else {
return image
}
}
static func convert(url: URL?, image: UIImage) -> UIImage? {
if let url,
let cached = cache.object(forKey: url as NSURL) {
@ -50,21 +35,6 @@ struct ImageGrayscalifier {
return doConvert(CIImage(cgImage: cgImage), url: url)
}
static func convert(url: URL?, image: UIImage) async -> UIImage? {
if let url,
let cached = cache.object(forKey: url as NSURL) {
return cached
}
guard let cgImage = image.cgImage else {
return nil
}
return await withCheckedContinuation { continuation in
queue.async {
continuation.resume(returning: doConvert(CIImage(cgImage: cgImage), url: url))
}
}
}
static func convert(url: URL?, data: Data) -> UIImage? {
if let url = url,
let cached = cache.object(forKey: url as NSURL) {
@ -86,18 +56,6 @@ struct ImageGrayscalifier {
return doConvert(CIImage(cgImage: cgImage), url: url)
}
static func convert(url: URL?, cgImage: CGImage) async -> UIImage? {
if let url = url,
let cached = cache.object(forKey: url as NSURL) {
return cached
}
return await withCheckedContinuation { continuation in
queue.async {
continuation.resume(returning: doConvert(CIImage(cgImage: cgImage), url: url))
}
}
}
private static func doConvert(_ source: CIImage, url: URL?) -> UIImage? {
guard let filter = CIFilter(name: "CIColorMonochrome") else {
return nil

View File

@ -1,23 +0,0 @@
//
// MainThreadBox.swift
// Tusker
//
// Created by Shadowfacts on 1/27/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import Foundation
struct MainThreadBox<T>: @unchecked Sendable {
private let _value: T
@MainActor
var value: T {
_value
}
@MainActor
init(value: T) {
self._value = value
}
}

View File

@ -8,7 +8,6 @@
import UIKit
@MainActor
struct MenuController {
static let composeCommand: UIKeyCommand = {

View File

@ -12,22 +12,15 @@ import os
// once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]>
// to make the lock semantics more clear
@available(iOS, obsoleted: 16.0)
@available(visionOS 1.0, *)
final class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
#if os(visionOS)
private let lock = OSAllocatedUnfairLock(initialState: [Key: Value]())
#else
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
private let lock: any Lock<[Key: Value]>
#endif
init() {
#if !os(visionOS)
if #available(iOS 16.0, *) {
self.lock = OSAllocatedUnfairLock(initialState: [:])
} else {
self.lock = UnfairLock(initialState: [:])
}
#endif
}
subscript(key: Key) -> Value? {
@ -37,15 +30,9 @@ final class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable>: @u
}
}
set(value) {
#if os(visionOS)
lock.withLock { dict in
dict[key] = value
}
#else
_ = lock.withLock { dict in
dict[key] = value
}
#endif
}
}
@ -70,7 +57,6 @@ final class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable>: @u
}
}
#if !os(visionOS)
// TODO: replace this only with OSAllocatedUnfairLock
@available(iOS, obsoleted: 16.0)
fileprivate protocol Lock<State> {
@ -101,4 +87,3 @@ fileprivate class UnfairLock<State>: Lock {
return try body(&state)
}
}
#endif

View File

@ -81,12 +81,10 @@ extension Color {
static let appFill = Color(uiColor: .appFill)
}
#if !os(visionOS)
@available(iOS, obsoleted: 17.0)
private let traitsKey: String = ["Traits", "Defined", "client", "_"].reversed().joined()
@available(iOS, obsoleted: 17.0)
private let key = "tusker_usePureBlackDarkMode"
#endif
@available(iOS 17.0, *)
private struct PureBlackDarkModeTrait: UITraitDefinition {
@ -99,15 +97,10 @@ extension UITraitCollection {
if #available(iOS 17.0, *) {
return self[PureBlackDarkModeTrait.self]
} else {
#if os(visionOS)
return true // unreachable
#else
return obsoletePureBlackDarkMode
#endif
}
}
#if !os(visionOS)
@available(iOS, obsoleted: 17.0)
var obsoletePureBlackDarkMode: Bool {
get {
@ -120,18 +113,13 @@ extension UITraitCollection {
setValue(dict, forKey: traitsKey)
}
}
#endif
convenience init(pureBlackDarkMode: Bool) {
if #available(iOS 17.0, visionOS 1.0, *) {
if #available(iOS 17.0, *) {
self.init(PureBlackDarkModeTrait.self, value: pureBlackDarkMode)
} else {
self.init()
#if os(visionOS)
// unreachable
#else
self.obsoletePureBlackDarkMode = pureBlackDarkMode
#endif
}
}
}

View File

@ -11,7 +11,6 @@ import Pachyderm
import TuskerPreferences
extension StatusSwipeAction {
@MainActor
func createAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
switch self {
case .reply:
@ -30,7 +29,6 @@ extension StatusSwipeAction {
}
}
@MainActor
protocol StatusSwipeActionContainer: UIView {
var mastodonController: MastodonController! { get }
var navigationDelegate: any TuskerNavigationDelegate { get }
@ -42,7 +40,6 @@ protocol StatusSwipeActionContainer: UIView {
func performReplyAction()
}
@MainActor
private func createReplyAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else {
return nil
@ -56,7 +53,6 @@ private func createReplyAction(status: StatusMO, container: StatusSwipeActionCon
return action
}
@MainActor
private func createFavoriteAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else {
return nil
@ -73,7 +69,6 @@ private func createFavoriteAction(status: StatusMO, container: StatusSwipeAction
return action
}
@MainActor
private func createReblogAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn,
container.canReblog else {
@ -91,7 +86,6 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
return action
}
@MainActor
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
MainActor.runUnsafely {
@ -106,7 +100,6 @@ private func createShareAction(status: StatusMO, container: StatusSwipeActionCon
return action
}
@MainActor
private func createBookmarkAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else {
return nil
@ -131,10 +124,11 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction
return action
}
@MainActor
private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
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)
}
action.image = UIImage(systemName: "safari")

View File

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
</dict>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>54BD.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
</dict>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeCrashData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -10,14 +10,10 @@ import Foundation
import Pachyderm
import CoreData
// TODO: remove this class eventually
class SavedDataManager: Codable {
@MainActor
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
@MainActor
private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
@MainActor
static func load() -> SavedDataManager? {
let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: archiveURL),
@ -27,7 +23,6 @@ class SavedDataManager: Codable {
return nil
}
@MainActor
static func destroy() throws {
try FileManager.default.removeItem(at: archiveURL)
}
@ -44,7 +39,6 @@ class SavedDataManager: Codable {
return s
}
@MainActor
private func save() {
let encoder = PropertyListEncoder()
let data = try? encoder.encode(self)
@ -52,6 +46,8 @@ class SavedDataManager: Codable {
}
func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws {
var changed = false
if let hashtags = savedHashtags[accountID] {
let objects: [[String: Any]] = hashtags.map {
["url": $0.url, "name": $0.name]
@ -59,6 +55,7 @@ class SavedDataManager: Codable {
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)
try context.execute(hashtagsReq)
savedHashtags.removeValue(forKey: accountID)
changed = true
}
if let instances = savedInstances[accountID] {
@ -68,6 +65,11 @@ class SavedDataManager: Codable {
let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects)
try context.execute(instancesReq)
savedInstances.removeValue(forKey: accountID)
changed = true
}
if changed {
save()
}
}
}

View File

@ -10,9 +10,7 @@ import UIKit
import Pachyderm
import MessageUI
import CoreData
#if canImport(Duckable)
import Duckable
#endif
import UserAccounts
import ComposeUI
@ -253,9 +251,6 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
let mastodonController = window!.windowScene!.session.mastodonController!
mastodonController.initialize()
#if os(visionOS)
return MainTabBarViewController(mastodonController: mastodonController)
#else
let split = MainSplitViewController(mastodonController: mastodonController)
if UIDevice.current.userInterfaceIdiom == .phone,
#available(iOS 16.0, *) {
@ -264,7 +259,6 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
} else {
return split
}
#endif
}
func createOnboardingUI() -> UIViewController {

View File

@ -7,11 +7,8 @@
//
import UIKit
#if !os(visionOS)
import Sentry
#endif
@MainActor
protocol TuskerSceneDelegate: UISceneDelegate {
var window: UIWindow? { get }
var rootViewController: TuskerRootViewController? { get }
@ -35,9 +32,6 @@ extension TuskerSceneDelegate {
guard let window else { return }
window.overrideUserInterfaceStyle = Preferences.shared.theme
window.tintColor = Preferences.shared.accentColor.color
#if os(visionOS)
window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode
#else
if #available(iOS 17.0, *) {
window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode
} else {
@ -51,6 +45,5 @@ extension TuskerSceneDelegate {
SentrySDK.capture(exception: exception)
}
}
#endif
}
}

View File

@ -11,7 +11,7 @@ import Pachyderm
class AccountFollowsListViewController: UIViewController, CollectionViewController {
private static nonisolated let pageSize = 40
private static let pageSize = 40
let accountID: String
let mastodonController: MastodonController

View File

@ -13,11 +13,11 @@ class AccountFollowsViewController: SegmentedPageViewController<AccountFollowsVi
let accountID: String
let mastodonController: MastodonController
init(accountID: String, initialPage: Mode = .following, mastodonController: MastodonController) {
init(accountID: String, mastodonController: MastodonController) {
self.accountID = accountID
self.mastodonController = mastodonController
super.init(pages: [.following, .followers], initialPage: initialPage) { mode in
super.init(pages: [.following, .followers]) { mode in
AccountFollowsListViewController(accountID: accountID, mastodonController: mastodonController, mode: mode)
}
}

View File

@ -0,0 +1,50 @@
//
// AttachmentPreviewViewController.swift
// Tusker
//
// Created by Shadowfacts on 3/20/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import TuskerComponents
class AttachmentPreviewViewController: UIViewController {
private let attachment: Attachment
private let sourceView: AttachmentView
init(sourceView: AttachmentView) {
self.attachment = sourceView.attachment
self.sourceView = sourceView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
if let data = ImageCache.attachments.getData(attachment.url),
let image = UIImage(data: data) {
let imageView: UIImageView
if attachment.url.pathExtension == "gif" {
let gifView = GIFImageView(image: image)
let controller = sourceView.gifController ?? GIFController(gifData: data)
controller.attach(to: gifView)
controller.startAnimating()
imageView = gifView
} else {
imageView = UIImageView(image: image)
}
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .black
view = imageView
preferredContentSize = image.size
} else {
view = UIActivityIndicatorView(style: .large)
}
}
}

View File

@ -0,0 +1,70 @@
//
// GalleryPlayerViewController.swift
// Tusker
//
// Created by Shadowfacts on 6/21/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import AVKit
import Pachyderm
class GalleryPlayerViewController: UIViewController {
let playerVC = AVPlayerViewController()
var attachment: Attachment!
private var isFirstAppearance = true
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
playerVC.allowsPictureInPicturePlayback = true
playerVC.view.translatesAutoresizingMaskIntoConstraints = false
addChild(playerVC)
playerVC.didMove(toParent: self)
view.addSubview(playerVC.view)
NSLayoutConstraint.activate([
playerVC.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
playerVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
playerVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
playerVC.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// starting while audio is already playing from another app often takes nearly a second,
// so do it on a background thread as to not block the UI
DispatchQueue.global(qos: .userInitiated).async {
AudioSessionHelper.enable()
AudioSessionHelper.setVideoPlayback()
DispatchQueue.main.async {
if self.isFirstAppearance {
self.isFirstAppearance = false
self.playerVC.player?.play()
}
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// starting often takes around half a second,
// so do it on a background thread as to not block the UI
DispatchQueue.global(qos: .userInitiated).async {
AudioSessionHelper.setDefault()
AudioSessionHelper.disable()
}
}
}

View File

@ -0,0 +1,177 @@
// GalleryViewController.swift
// Tusker
//
// Created by Shadowfacts on 6/13/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import AVFoundation
import AVKit
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController {
weak var avPlayerViewControllerDelegate: AVPlayerViewControllerDelegate?
let attachments: [Attachment]
let sourceViews: WeakArray<UIImageView>
let startIndex: Int
var pages: [UIViewController]!
var currentIndex: Int {
guard let vc = viewControllers?.first,
let index = pages.firstIndex(of: vc) else {
fatalError()
}
return index
}
var animationSourceView: UIImageView? { sourceViews[currentIndex] }
var largeImageController: LargeImageViewController? {
// use protocol because page controllers may be loading or non-loading VCs
(pages[currentIndex] as? LargeImageAnimatableViewController)?.largeImageController
}
var animationImage: UIImage? {
if let page = pages[currentIndex] as? LargeImageAnimatableViewController,
let image = page.animationImage {
return image
} else {
return animationSourceView?.image
}
}
var dismissInteractionController: LargeImageInteractionController?
var isInteractivelyAnimatingDismissal: Bool = false {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
return !isInteractivelyAnimatingDismissal
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .none
}
override var childForHomeIndicatorAutoHidden: UIViewController? {
return viewControllers?.first
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
init(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) {
self.attachments = attachments
self.sourceViews = WeakArray(sourceViews)
self.startIndex = startIndex
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
modalPresentationStyle = .fullScreen
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.pages = attachments.enumerated().map { (index, attachment) in
switch attachment.kind {
case .image:
let vc = LoadingLargeImageViewController(attachment: attachment)
vc.shrinkGestureEnabled = false
vc.animationSourceView = sourceViews[index]
return vc
case .video, .audio:
let vc = GalleryPlayerViewController()
vc.playerVC.player = AVPlayer(url: attachment.url)
vc.playerVC.delegate = avPlayerViewControllerDelegate
vc.attachment = attachment
return vc
case .gifv:
// Passing the source view to the LargeImageGifvContentView is a crappy workaround for not
// having the current frame to use as the animationImage. This will break when there
// are more than 4 attachments and there is a gifv at index >= 3 (the More... button will show
// in place of the fourth attachment, so there aren't source views for the attachments at index >= 3).
// This isn't a priority as only Mastodon converts gifs to gifvs, and Mastodon (in its default configuration,
// I don't know about forks) doesn't allow more than four attachments, meaning there will always be a source view.
let gifvContentView = LargeImageGifvContentView(attachment: attachment, source: sourceViews[index]!)
let vc = LargeImageViewController(contentView: gifvContentView, description: attachment.description, sourceView: nil)
vc.shrinkGestureEnabled = false
return vc
default:
return UINavigationController(rootViewController: GalleryFallbackViewController(attachment: attachment))
}
}
setViewControllers([pages[startIndex]], direction: .forward, animated: false)
self.dataSource = self
self.delegate = self
overrideUserInterfaceStyle = .dark
dismissInteractionController = LargeImageInteractionController(viewController: self)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let vc = pages[currentIndex] as? AVPlayerViewController {
// when the gallery is first shown, after the transition finishes, the controls for the player controller appear semi-transparent
// hiding the controls and then immediately reshowing them makes sure they're visible when the gallery is presented
vc.showsPlaybackControls = false
vc.showsPlaybackControls = true
// begin playing the video as soon as we appear
vc.player?.play()
}
}
override func accessibilityPerformEscape() -> Bool {
dismiss(animated: true)
return true
}
// MARK: - Page View Controller Data Source
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = pages.firstIndex(of: viewController),
index > 0 else {
return nil
}
return pages[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = pages.firstIndex(of: viewController),
index < pages.count - 1 else {
return nil
}
return pages[index + 1]
}
// MARK: - Page View Controller Delegate
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
if let pending = pendingViewControllers.first as? LoadingLargeImageViewController,
let current = viewControllers!.first as? LoadingLargeImageViewController {
pending.controlsVisible = current.controlsVisible
}
if let pending = pendingViewControllers.first as? AVPlayerViewController {
// show controls and begin playing when the player page becomes visible
pending.showsPlaybackControls = true
pending.player?.play()
}
}
}

View File

@ -0,0 +1,33 @@
//
// GifvAttachmentViewController.swift
// Tusker
//
// Created by Shadowfacts on 5/12/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import AVFoundation
class GifvAttachmentViewController: UIViewController {
private let attachment: Attachment
init(attachment: Attachment) {
precondition(attachment.kind == .gifv)
self.attachment = attachment
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let asset = AVURLAsset(url: attachment.url)
self.view = GifvAttachmentView(asset: asset, gravity: .resizeAspect)
}
}

View File

@ -9,7 +9,6 @@
import UIKit
import PencilKit
@MainActor
protocol ComposeDrawingViewControllerDelegate: AnyObject {
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)

View File

@ -14,16 +14,13 @@ import PhotosUI
import PencilKit
import Pachyderm
import CoreData
#if canImport(Duckable)
import Duckable
#endif
@MainActor
protocol ComposeHostingControllerDelegate: AnyObject {
func dismissCompose(mode: DismissMode) -> Bool
}
class ComposeHostingController: UIHostingController<ComposeHostingController.View> {
class ComposeHostingController: UIHostingController<ComposeHostingController.View>, DuckableViewController {
weak var delegate: ComposeHostingControllerDelegate?
@ -144,23 +141,8 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
present(ComposeDrawingNavigationController(editing: drawing, delegate: self), animated: true)
}
struct View: SwiftUI.View {
let mastodonController: MastodonController
let controller: ComposeController
var body: some SwiftUI.View {
ControllerView(controller: { controller })
.task {
if let account = try? await mastodonController.getOwnAccount() {
controller.currentAccount = account
}
}
}
}
}
#if canImport(Duckable)
extension ComposeHostingController: DuckableViewController {
// MARK: Duckable
func duckableViewControllerShouldDuck() -> DuckAttemptAction {
if controller.isPosting {
return .block
@ -182,8 +164,21 @@ extension ComposeHostingController: DuckableViewController {
func duckableViewControllerDidFinishAnimatingDuck() {
controller.showToolbar = true
}
struct View: SwiftUI.View {
let mastodonController: MastodonController
let controller: ComposeController
var body: some SwiftUI.View {
ControllerView(controller: { controller })
.task {
if let account = try? await mastodonController.getOwnAccount() {
controller.currentAccount = account
}
}
}
}
}
#endif
extension MastodonController: ComposeMastodonContext {
@MainActor

View File

@ -28,9 +28,8 @@ struct ComposeReplyContentView: UIViewRepresentable {
view.adjustsFontForContentSizeCategory = true
view.overrideMastodonController = mastodonController
let content = TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
view.attributedText = content
view.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
return view
}

View File

@ -296,7 +296,7 @@ extension ConversationCollectionViewController {
case mainStatus
case childThread(firstStatusID: String)
}
enum Item: Hashable, Sendable {
enum Item: Hashable {
case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool)
case expandThread(childThreads: [ConversationNode], inline: Bool)
case loadingIndicator
@ -306,7 +306,7 @@ extension ConversationCollectionViewController {
case let (.status(id: a, node: _, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, node: _, state: _, prevLink: bPrev, nextLink: bNext)):
return a == b && aPrev == bPrev && aNext == bNext
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
return a.count == b.count && zip(a, b).allSatisfy { $0.statusID == $1.statusID } && aInline == bInline
return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
case (.loadingIndicator, .loadingIndicator):
return true
default:
@ -324,7 +324,7 @@ extension ConversationCollectionViewController {
case .expandThread(childThreads: let childThreads, inline: let inline):
hasher.combine(1)
for thread in childThreads {
hasher.combine(thread.statusID)
hasher.combine(thread.status.id)
}
hasher.combine(inline)
case .loadingIndicator:
@ -413,12 +413,6 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
}
#if os(visionOS)
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
return self.collectionView(collectionView, shouldSelectItemAt: indexPath)
}
#endif
}
extension ConversationCollectionViewController: UICollectionViewDragDelegate {

View File

@ -9,20 +9,15 @@
import Foundation
import Pachyderm
@MainActor
class ConversationNode {
let statusID: String
let status: StatusMO
var children: [ConversationNode]
init(status: StatusMO) {
self.statusID = status.id
self.status = status
self.children = []
}
}
@MainActor
struct ConversationTree {
let ancestors: [ConversationNode]
let mainStatus: ConversationNode

View File

@ -194,7 +194,7 @@ class ConversationViewController: UIViewController {
if let cached = mastodonController.persistentContainer.status(for: mainStatusID) {
// if we have a cached copy, display it immediately but still try to refresh it
Task {
_ = await doLoadMainStatus()
await doLoadMainStatus()
}
mainStatusLoaded(cached)
} else {
@ -216,7 +216,7 @@ class ConversationViewController: UIViewController {
state = .loading(indicator)
let effectiveURL: String
final class RedirectBlocker: NSObject, URLSessionTaskDelegate, Sendable {
class RedirectBlocker: NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
completionHandler(nil)
}

View File

@ -166,15 +166,13 @@ class IssueReporterViewController: UIViewController {
}
extension IssueReporterViewController: MFMailComposeViewControllerDelegate {
nonisolated func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
MainActor.runUnsafely {
controller.dismiss(animated: true) {
if result == .cancelled {
// don't dismiss ourself, to allowe the user to send the report a different way
} else {
self.finishedReport()
self.doDismiss()
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) {
if result == .cancelled {
// don't dismiss ourself, to allowe the user to send the report a different way
} else {
self.finishedReport()
self.doDismiss()
}
}
}

View File

@ -49,9 +49,7 @@ struct AddHashtagPinnedTimelineView: View {
var body: some View {
NavigationView {
list
#if !os(visionOS)
.appGroupedListBackground(container: AddHashtagPinnedTimelineRepresentable.UIViewControllerType.self)
#endif
.listStyle(.grouped)
.navigationTitle("Add Hashtag")
.navigationBarTitleDisplayMode(.inline)

View File

@ -148,9 +148,7 @@ struct EditFilterView: View {
.appGroupedListRowBackground()
}
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
#if !os(visionOS)
.scrollDismissesKeyboardInteractivelyIfAvailable()
#endif
.navigationTitle(create ? "Add Filter" : "Edit Filter")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@ -171,21 +169,12 @@ struct EditFilterView: View {
}, message: { error in
Text(error.localizedDescription)
})
#if os(visionOS)
.onChange(of: expiresIn) {
edited = true
if expires.wrappedValue {
filter.expiresIn = expiresIn
}
}
#else
.onChange(of: expiresIn, perform: { newValue in
edited = true
if expires.wrappedValue {
filter.expiresIn = newValue
}
})
#endif
.onReceive(filter.objectWillChange, perform: { _ in
edited = true
})

View File

@ -111,10 +111,6 @@ struct PinnedTimelinesView: View {
Text("Pinned Timelines")
}
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
#if os(visionOS)
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
.edgesIgnoringSafeArea(.bottom)
#else
if #available(iOS 16.0, *) {
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
.edgesIgnoringSafeArea(.bottom)
@ -122,7 +118,6 @@ struct PinnedTimelinesView: View {
AddHashtagPinnedTimelineRepresentable(pinnedTimelines: $pinnedTimelines)
.edgesIgnoringSafeArea(.bottom)
}
#endif
})
.sheet(isPresented: $isShowingAddInstanceSheet, content: {
AddInstancePinnedTimelineView(pinnedTimelines: $pinnedTimelines)
@ -133,19 +128,11 @@ struct PinnedTimelinesView: View {
pinnedTimelines = accountPreferences.pinnedTimelines
}
}
#if os(visionOS)
.onChange(of: pinnedTimelines) {
if accountPreferences.pinnedTimelines != pinnedTimelines {
accountPreferences.pinnedTimelines = pinnedTimelines
}
}
#else
.onChange(of: pinnedTimelines) { newValue in
if accountPreferences.pinnedTimelines != newValue {
accountPreferences.pinnedTimelines = newValue
}
}
#endif
}
}

View File

@ -14,7 +14,7 @@ import WebURLFoundationExtras
class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {
private let mastodonController: MastodonController
weak var mastodonController: MastodonController!
var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

View File

@ -20,11 +20,8 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
var account: Account?
private var accountImagesTask: Task<Void, Never>?
deinit {
accountImagesTask?.cancel()
}
private var avatarRequest: ImageCache.Request?
private var headerRequest: ImageCache.Request?
override func awakeFromNib() {
super.awakeFromNib()
@ -65,35 +62,37 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
noteTextView.setEmojis(account.emojis, identifier: account.id)
avatarImageView.image = nil
headerImageView.image = nil
accountImagesTask?.cancel()
accountImagesTask = Task {
await updateImages(account: account)
}
}
private nonisolated func updateImages(account: Account) async {
await withTaskGroup(of: Void.self) { group in
group.addTask {
guard let avatar = account.avatar,
let image = await ImageCache.avatars.get(avatar).1 else {
if let avatar = account.avatar {
avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
defer {
self?.avatarRequest = nil
}
guard let self = self,
let image = image,
self.account?.id == account.id else {
return
}
await MainActor.run {
DispatchQueue.main.async {
self.avatarImageView.image = image
}
}
group.addTask {
guard let header = account.header,
let image = await ImageCache.headers.get(header).1 else {
}
headerImageView.image = nil
if let header = account.header {
headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in
defer {
self?.headerRequest = nil
}
guard let self = self,
let image = image,
self.account?.id == account.id else {
return
}
await MainActor.run {
DispatchQueue.main.async {
self.headerImageView.image = image
}
}
await group.waitForAll()
}
}
@ -107,13 +106,10 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
}
}
// Unneeded on visionOS because there is no light/dark mode
#if !os(visionOS)
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors()
}
#endif
override func layoutSubviews() {
super.layoutSubviews()

View File

@ -0,0 +1,233 @@
//
// ProfileDirectoryViewController.swift
// Tusker
//
// Created by Shadowfacts on 2/6/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class ProfileDirectoryViewController: UIViewController {
weak var mastodonController: MastodonController!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var scope: Scope = .everywhere
private var order: DirectoryOrder = .active
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("Profile Directory", comment: "profile directory title")
let filterItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), menu: nil)
filterItem.accessibilityLabel = "Filter"
navigationItem.rightBarButtonItem = filterItem
updateFilterMenu()
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) in
let itemHeight = NSCollectionLayoutDimension.absolute(200)
let itemWidth: NSCollectionLayoutDimension
if case .compact = layoutEnvironment.traitCollection.horizontalSizeClass {
itemWidth = .fractionalWidth(1)
} else {
itemWidth = .absolute((layoutEnvironment.container.contentSize.width - 16 - 8 * 2) / 2)
}
let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemHeight)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let itemB = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: itemHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, itemB])
group.interItemSpacing = .flexible(16)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 16
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
return section
})
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .appSecondaryBackground
collectionView.register(UINib(nibName: "FeaturedProfileCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: "featuredProfileCell")
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.allowsFocus = true
view.addSubview(collectionView)
dataSource = createDataSource()
updateProfiles()
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
guard case let .account(account) = item else { fatalError() }
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "featuredProfileCell", for: indexPath) as! FeaturedProfileCollectionViewCell
cell.updateUI(account: account)
return cell
}
return dataSource
}
private func updateFilterMenu() {
let scopeMenu = UIMenu(options: .displayInline, children: [
UIAction(title: "Everywhere", subtitle: "Users from the whole network", image: UIImage(systemName: "globe"), state: scope == .everywhere ? .on : .off, handler: { [unowned self] _ in
self.scope = .everywhere
self.updateFilterMenu()
self.updateProfiles()
}),
UIAction(title: mastodonController.accountInfo!.instanceURL.host!, subtitle: "Only users from your instance", image: UIImage(systemName: "house"), state: scope == .instance ? .on : .off, handler: { [unowned self] _ in
self.scope = .instance
self.updateFilterMenu()
self.updateProfiles()
}),
])
let orderMenu = UIMenu(options: .displayInline, children: DirectoryOrder.allCases.map { order in
UIAction(title: order.title, subtitle: order.subtitle, image: nil, state: self.order == order ? .on : .off) { [unowned self] _ in
self.order = order
self.updateFilterMenu()
self.updateProfiles()
}
})
navigationItem.rightBarButtonItem!.menu = UIMenu(children: [
scopeMenu,
orderMenu,
])
}
private func updateProfiles() {
let scope = self.scope
let order = self.order
let local = scope == .instance
let request = Client.getFeaturedProfiles(local: local, order: order)
mastodonController.run(request) { (response) in
guard case let .success(accounts, _) = response,
self.scope == scope,
self.order == order else {
return
}
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.featuredProfiles])
snapshot.appendItems(accounts.map { .account($0) })
DispatchQueue.main.async {
self.dataSource.apply(snapshot)
}
}
}
}
extension ProfileDirectoryViewController {
enum Section {
case featuredProfiles
}
enum Item: Hashable {
case account(Account)
func hash(into hasher: inout Hasher) {
guard case let .account(account) = self else { return }
hasher.combine(account.id)
}
}
}
extension ProfileDirectoryViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController }
}
extension ProfileDirectoryViewController: ToastableViewController {
}
extension ProfileDirectoryViewController: MenuActionProvider {
}
extension ProfileDirectoryViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .account(account) = item else {
return
}
show(ProfileViewController(accountID: account.id, mastodonController: mastodonController), sender: nil)
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .account(account) = item else {
return nil
}
return UIContextMenuConfiguration(identifier: nil) {
return ProfileViewController(accountID: account.id, mastodonController: self.mastodonController)
} actionProvider: { (_) in
let actions = self.actionsForProfile(accountID: account.id, source: .view(self.collectionView.cellForItem(at: indexPath)))
return UIMenu(children: actions)
}
}
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
if let viewController = animator.previewViewController {
animator.preferredCommitStyle = .pop
animator.addCompletion {
self.show(viewController, sender: nil)
}
}
}
}
extension ProfileDirectoryViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let item = dataSource.itemIdentifier(for: indexPath),
case let .account(account) = item,
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)]
}
}
extension ProfileDirectoryViewController {
enum Scope: CaseIterable {
case instance, everywhere
}
}
private extension DirectoryOrder {
var title: String {
switch self {
case .active:
return "Active"
case .new:
return "New"
}
}
var subtitle: String {
switch self {
case .active:
return "Users who have posted lately"
case .new:
return "Recently joined users"
}
}
}

View File

@ -6,7 +6,6 @@
// Copyright © 2023 Shadowfacts. All rights reserved.
//
#if !os(visionOS)
import UIKit
import Pachyderm
import SwiftUI
@ -99,13 +98,10 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
displayNameLabel.updateForAccountDisplayName(account: account)
}
// Unneeded on visionOS since there is no light/dark mode
#if !os(visionOS)
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors()
}
#endif
private func updateLayerColors() {
if traitCollection.userInterfaceStyle == .dark {
@ -128,12 +124,10 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact {
toPresent = UINavigationController(rootViewController: host)
toPresent.modalPresentationStyle = .pageSheet
#if !os(visionOS)
let sheetPresentationController = toPresent.sheetPresentationController!
sheetPresentationController.detents = [
.medium()
]
#endif
} else {
host.modalPresentationStyle = .popover
let popoverPresentationController = host.popoverPresentationController!
@ -222,4 +216,3 @@ private struct SuggestionSourceView: View {
}
}
}
#endif

View File

@ -1,78 +0,0 @@
//
// SuggestedProfileCardView.swift
// Tusker
//
// Created by Shadowfacts on 11/8/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
#if os(visionOS)
import SwiftUI
struct SuggestedProfileCardView: View {
let account: AccountMO
var body: some View {
VStack {
HeaderLayout {
AsyncImage(url: account.header) { image in
image
.resizable()
} placeholder: {
Rectangle().fill(.tertiary)
}
AsyncImage(url: account.avatar) { image in
image
.resizable()
.clipShape(RoundedRectangle(cornerRadius: 5))
} placeholder: {
Rectangle().fill(.tertiary)
}
VStack(alignment: .leading) {
AccountDisplayNameView(account: account, textStyle: .title, emojiSize: 24)
Text(verbatim: "@\(account.acct)")
}
}
NoteTextView(note: account.note)
}
.glassBackgroundEffect(in: RoundedRectangle(cornerRadius: 12.5))
}
}
private struct HeaderLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let acceptedWidth = proposal.width ?? 200
let avatarSize = subviews[1].sizeThatFits(ProposedViewSize(width: 86, height: 86))
let accountInfoSize = subviews[2].sizeThatFits(ProposedViewSize(width: acceptedWidth - 8 - avatarSize.width, height: 43))
return CGSize(width: proposal.width ?? 200, height: 100 + 4 + accountInfoSize.height)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let headerSize = subviews[0].sizeThatFits(ProposedViewSize(width: bounds.width, height: 100))
subviews[0].place(at: .zero, proposal: ProposedViewSize(headerSize))
let avatarSize = subviews[1].sizeThatFits(ProposedViewSize(width: 86, height: 86))
subviews[1].place(at: CGPoint(x: 8, y: headerSize.height), anchor: .leading, proposal: ProposedViewSize(avatarSize))
subviews[2].place(at: CGPoint(x: 8 + avatarSize.width + 8, y: headerSize.height + 4), proposal: ProposedViewSize(width: bounds.width - 8 - avatarSize.width - 8, height: 43))
}
}
private struct NoteTextView: UIViewRepresentable {
typealias UIViewType = ContentTextView
let note: String
func makeUIView(context: Context) -> ContentTextView {
let view = ContentTextView()
view.isUserInteractionEnabled = false
view.isScrollEnabled = true
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return view
}
func updateUIView(_ uiView: ContentTextView, context: Context) {
uiView.setBodyTextFromHTML(note)
}
}
//#Preview {
// SuggestedProfileCardView()
//}
#endif

View File

@ -8,13 +8,10 @@
import UIKit
import Pachyderm
#if os(visionOS)
import SwiftUI
#endif
class SuggestedProfilesViewController: UIViewController, CollectionViewController {
private let mastodonController: MastodonController
weak var mastodonController: MastodonController!
var collectionView: UICollectionView!
private var layout: MultiColumnCollectionViewLayout!
@ -56,22 +53,10 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
}
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
#if os(visionOS)
let accountCell = UICollectionView.CellRegistration<UICollectionViewCell, (String, Suggestion.Source)> { [unowned self] cell, indexPath, item in
if let account = self.mastodonController.persistentContainer.account(for: item.0) {
cell.contentConfiguration = UIHostingConfiguration(content: {
SuggestedProfileCardView(account: account)
})
} else {
cell.contentConfiguration = nil
}
}
#else
let accountCell = UICollectionView.CellRegistration<SuggestedProfileCardCollectionViewCell, (String, Suggestion.Source)>(cellNib: UINib(nibName: "SuggestedProfileCardCollectionViewCell", bundle: .main)) { cell, indexPath, item in
cell.delegate = self
cell.updateUI(accountID: item.0, source: item.1)
}
#endif
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .account(let id, let source):
@ -110,9 +95,7 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts])
await MainActor.run {
dataSource.apply(snapshot)
}
await dataSource.apply(snapshot)
do {
let request = Client.getSuggestions(limit: 80)
@ -125,9 +108,7 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts])
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) })
await MainActor.run {
dataSource.apply(snapshot)
}
await dataSource.apply(snapshot)
state = .loaded
} catch {

View File

@ -13,7 +13,7 @@ import Combine
class TrendingHashtagsViewController: UIViewController {
private let mastodonController: MastodonController
weak var mastodonController: MastodonController!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
@ -107,9 +107,7 @@ class TrendingHashtagsViewController: UIViewController {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.trendingTags])
snapshot.appendItems([.loadingIndicator])
await MainActor.run {
dataSource.apply(snapshot)
}
await dataSource.apply(snapshot)
do {
let request = self.request(offset: nil)
@ -117,14 +115,10 @@ class TrendingHashtagsViewController: UIViewController {
snapshot.deleteItems([.loadingIndicator])
snapshot.appendItems(hashtags.map { .tag($0) })
state = .loaded
await MainActor.run {
dataSource.apply(snapshot)
}
await dataSource.apply(snapshot)
} catch {
snapshot.deleteItems([.loadingIndicator])
await MainActor.run {
dataSource.apply(snapshot)
}
await dataSource.apply(snapshot)
state = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Trending Tags", in: self) { [weak self] toast in
@ -146,9 +140,7 @@ class TrendingHashtagsViewController: UIViewController {
var snapshot = origSnapshot
if Preferences.shared.disableInfiniteScrolling {
snapshot.appendItems([.confirmLoadMore(false)])
await MainActor.run {
dataSource.apply(snapshot)
}
await dataSource.apply(snapshot)
for await _ in confirmLoadMore.values {
break
@ -156,14 +148,10 @@ class TrendingHashtagsViewController: UIViewController {
snapshot.deleteItems([.confirmLoadMore(false)])
snapshot.appendItems([.confirmLoadMore(true)])
await MainActor.run {
dataSource.apply(snapshot, animatingDifferences: false)
}
await dataSource.apply(snapshot, animatingDifferences: false)
} else {
snapshot.appendItems([.loadingIndicator])
await MainActor.run {
dataSource.apply(snapshot)
}
await dataSource.apply(snapshot)
}
do {
@ -171,13 +159,9 @@ class TrendingHashtagsViewController: UIViewController {
let (hashtags, _) = try await mastodonController.run(request)
var snapshot = origSnapshot
snapshot.appendItems(hashtags.map { .tag($0) })
await MainActor.run {
dataSource.apply(snapshot)
}
await dataSource.apply(snapshot)
} catch {
await MainActor.run {
dataSource.apply(origSnapshot)
}
await dataSource.apply(origSnapshot)
let config = ToastConfiguration(from: error, with: "Error Loading More Tags", in: self) { [weak self] toast in
toast.dismissToast(animated: true)

View File

@ -6,7 +6,6 @@
// Copyright © 2022 Shadowfacts. All rights reserved.
//
#if !os(visionOS)
import UIKit
import Pachyderm
import WebURLFoundationExtras
@ -79,7 +78,7 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
providerLabel.text = provider
let converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
descriptionLabel.text = converter.convert(html: card.description)
descriptionLabel.isHidden = description.isEmpty
@ -130,13 +129,10 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
}
}
// Unneeded on visionOS because there is no light/dark mode
#if !os(visionOS)
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors()
}
#endif
private func updateLayerColors() {
if traitCollection.userInterfaceStyle == .dark {
@ -173,4 +169,3 @@ extension TrendingLinkCardCollectionViewCell {
case regular, compact
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More