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 # 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) ## 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! 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 UIKit
import MobileCoreServices import MobileCoreServices
import UniformTypeIdentifiers
class ActionViewController: UIViewController { class ActionViewController: UIViewController {
@ -18,29 +17,25 @@ class ActionViewController: UIViewController {
super.viewDidLoad() super.viewDidLoad()
findURLFromWebPage { (components) in findURLFromWebPage { (components) in
DispatchQueue.main.async { if let components = components {
if let components {
self.searchForURLInApp(components) self.searchForURLInApp(components)
} else { } else {
self.findURLItem { (components) in self.findURLItem { (components) in
if let components { if let components = components {
DispatchQueue.main.async {
self.searchForURLInApp(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 item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! { for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else { guard provider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) else {
continue 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], guard let result = result as? [String: Any],
let jsResult = result[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any], let jsResult = result[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any],
let urlString = jsResult["activityPubURL"] as? String ?? jsResult["url"] as? String, let urlString = jsResult["activityPubURL"] as? String ?? jsResult["url"] as? String,
@ -58,13 +53,13 @@ class ActionViewController: UIViewController {
completion(nil) 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 item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! { for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) else { guard provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) else {
continue 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, guard let result = result as? URL,
let components = URLComponents(url: result, resolvingAgainstBaseURL: false) else { let components = URLComponents(url: result, resolvingAgainstBaseURL: false) else {
completion(nil) 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 } var instanceFeatures: InstanceFeatures { get }
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?)
func getCustomEmojis(completion: @escaping ([Emoji]) -> Void)
func getCustomEmojis() async -> [Emoji]
@MainActor @MainActor
func searchCachedAccounts(query: String) -> [AccountProtocol] func searchCachedAccounts(query: String) -> [AccountProtocol]

View File

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

View File

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

View File

@ -85,11 +85,8 @@ class AttachmentsListController: ViewController {
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) { for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
provider.loadObject(ofClass: DraftAttachment.self) { object, error in provider.loadObject(ofClass: DraftAttachment.self) { object, error in
guard let attachment = object as? DraftAttachment else { return } guard let attachment = object as? DraftAttachment else { return }
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async {
guard let self, guard self.canAddAttachment else { return }
self.canAddAttachment else {
return
}
DraftsPersistentContainer.shared.viewContext.insert(attachment) DraftsPersistentContainer.shared.viewContext.insert(attachment)
attachment.draft = self.draft attachment.draft = self.draft
self.draft.attachments.add(attachment) self.draft.attachments.add(attachment)
@ -134,9 +131,9 @@ class AttachmentsListController: ViewController {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View { var body: some View {
Group {
attachmentsList attachmentsList
Group {
if controller.parent.config.presentAssetPicker != nil { if controller.parent.config.presentAssetPicker != nil {
addImageButton addImageButton
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) .listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
@ -150,10 +147,6 @@ class AttachmentsListController: ViewController {
togglePollButton togglePollButton
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2)) .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 { 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 @MainActor
private func queryChanged(_ query: String) async { 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 { guard !Task.isCancelled else {
return return
} }

View File

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

View File

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

View File

@ -45,52 +45,16 @@ class ToolbarController: ViewController {
@EnvironmentObject private var composeController: ComposeController @EnvironmentObject private var composeController: ComposeController
@ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22 @ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22
#if !os(visionOS)
@State private var minWidth: CGFloat? @State private var minWidth: CGFloat?
@State private var realWidth: CGFloat? @State private var realWidth: CGFloat?
#endif
var body: some View { var body: some View {
#if os(visionOS)
buttons
#else
ScrollView(.horizontal, showsIndicators: false) { 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
}
})
}
.scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0)
.frame(height: ToolbarController.height)
.frame(maxWidth: .infinity)
.background(.regularMaterial, ignoresSafeAreaEdges: [.bottom, .leading, .trailing])
.overlay(alignment: .top) {
Divider()
.edgesIgnoringSafeArea([.leading, .trailing])
}
.background(GeometryReader { proxy in
Color.clear
.preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(ToolbarWidthPrefKey.self) { width in
minWidth = width
}
})
#endif
}
@ViewBuilder
private var buttons: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
cwButton cwButton
MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly) MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly)
#if !targetEnvironment(macCatalyst) && !os(visionOS) #if !targetEnvironment(macCatalyst)
// the button has a bunch of extra space by default, but combined with what we add it's too much // the button has a bunch of extra space by default, but combined with what we add it's too much
.padding(.horizontal, -8) .padding(.horizontal, -8)
#endif #endif
@ -101,7 +65,7 @@ class ToolbarController: ViewController {
localOnlyPicker localOnlyPicker
#if targetEnvironment(macCatalyst) #if targetEnvironment(macCatalyst)
.padding(.leading, 4) .padding(.leading, 4)
#elseif !os(visionOS) #else
.padding(.horizontal, -8) .padding(.horizontal, -8)
#endif #endif
.disabled(draft.editedStatusID != nil) .disabled(draft.editedStatusID != nil)
@ -127,6 +91,31 @@ class ToolbarController: ViewController {
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection) 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)
.frame(maxWidth: .infinity)
.background(.regularMaterial, ignoresSafeAreaEdges: [.bottom, .leading, .trailing])
.overlay(alignment: .top) {
Divider()
.edgesIgnoringSafeArea([.leading, .trailing])
}
.background(GeometryReader { proxy in
Color.clear
.preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(ToolbarWidthPrefKey.self) { width in
minWidth = width
}
})
} }
private var cwButton: some View { private var cwButton: some View {

View File

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

View File

@ -11,7 +11,7 @@ import PencilKit
extension PKDrawing { 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) let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light)
var drawingImage: UIImage! var drawingImage: UIImage!
lightTraitCollection.performAsCurrent { lightTraitCollection.performAsCurrent {

View File

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

View File

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

View File

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

View File

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

View File

@ -23,41 +23,19 @@ struct MainTextView: View {
controller.config 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 { var body: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
MainWrappedTextViewRepresentable( colorScheme == .dark ? config.fillColor : Color(uiColor: .secondarySystemBackground)
text: $draft.text,
backgroundColor: textViewBackgroundColor,
becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder,
updateSelection: $updateSelection,
textDidChange: textDidChange
)
if draft.text.isEmpty { if draft.text.isEmpty {
ControllerView(controller: { PlaceholderController() }) ControllerView(controller: { PlaceholderController() })
.font(.system(size: fontSize)) .font(.system(size: fontSize))
.foregroundColor(.secondary) .foregroundColor(.secondary)
.offset(placeholderOffset) .offset(x: 4, y: 8)
.accessibilityHidden(true) .accessibilityHidden(true)
.allowsHitTesting(false)
} }
MainWrappedTextViewRepresentable(text: $draft.text, becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder, updateSelection: $updateSelection, textDidChange: textDidChange)
} }
.frame(height: effectiveHeight) .frame(height: effectiveHeight)
.onAppear(perform: becomeFirstResponderOnFirstAppearance) .onAppear(perform: becomeFirstResponderOnFirstAppearance)
@ -84,7 +62,6 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
typealias UIViewType = UITextView typealias UIViewType = UITextView
@Binding var text: String @Binding var text: String
let backgroundColor: UIColor?
@Binding var becomeFirstResponder: Bool @Binding var becomeFirstResponder: Bool
@Binding var updateSelection: ((UITextView) -> Void)? @Binding var updateSelection: ((UITextView) -> Void)?
let textDidChange: (UITextView) -> Void let textDidChange: (UITextView) -> Void
@ -97,16 +74,10 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
context.coordinator.textView = textView context.coordinator.textView = textView
textView.delegate = context.coordinator textView.delegate = context.coordinator
textView.isEditable = true textView.isEditable = true
textView.backgroundColor = .clear
textView.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 20)) textView.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 20))
textView.adjustsFontForContentSizeCategory = true textView.adjustsFontForContentSizeCategory = true
textView.textContainer.lineBreakMode = .byWordWrapping 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 return textView
} }
@ -119,8 +90,6 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
uiView.isEditable = isEnabled uiView.isEditable = isEnabled
uiView.keyboardType = controller.config.useTwitterKeyboard ? .twitter : .default uiView.keyboardType = controller.config.useTwitterKeyboard ? .twitter : .default
uiView.backgroundColor = backgroundColor
context.coordinator.text = $text context.coordinator.text = $text
if let updateSelection { if let updateSelection {

View File

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

View File

@ -62,9 +62,7 @@ public class DuckableContainerViewController: UIViewController {
guard case .idle = state else { guard case .idle = state else {
if animated, if animated,
case .ducked(_, placeholder: let placeholder) = state { case .ducked(_, placeholder: let placeholder) = state {
#if !os(visionOS)
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
#endif
let origConstant = placeholder.topConstraint.constant let origConstant = placeholder.topConstraint.constant
UIView.animateKeyframes(withDuration: 0.4, delay: 0) { UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) { 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 Combine
import Pachyderm import Pachyderm
public final class InstanceFeatures: ObservableObject { public class InstanceFeatures: ObservableObject {
private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; (pleroma|akkoma) (.*)\\)", options: .caseInsensitive) private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; (pleroma|akkoma) (.*)\\)", options: .caseInsensitive)
private let _featuresUpdated = PassthroughSubject<Void, Never>() private let _featuresUpdated = PassthroughSubject<Void, Never>()

View File

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

View File

@ -12,7 +12,7 @@ import WebURL
/** /**
The base Mastodon API client. The base Mastodon API client.
*/ */
public struct Client: Sendable { public class Client {
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
@ -20,6 +20,8 @@ public struct Client: Sendable {
let session: URLSession let session: URLSession
public var accessToken: String? public var accessToken: String?
public var appID: String?
public var clientID: String? public var clientID: String?
public var clientSecret: String? public var clientSecret: String?
@ -59,11 +61,9 @@ public struct Client: Sendable {
return encoder 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.baseURL = baseURL
self.accessToken = accessToken self.accessToken = accessToken
self.clientID = clientID
self.clientSecret = clientSecret
self.session = session self.session = session
} }
@ -150,7 +150,14 @@ public struct Client: Sendable {
"scopes" => scopes.scopeString, "scopes" => scopes.scopeString,
"website" => website?.absoluteString "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>) { public func getAccessToken(authorizationCode: String, redirectURI: String, scopes: [Scope], completion: @escaping Callback<LoginSettings>) {
@ -162,7 +169,12 @@ public struct Client: Sendable {
"redirect_uri" => redirectURI, "redirect_uri" => redirectURI,
"scope" => scopes.scopeString, "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 { 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 wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
let wellKnownResults = try await run(wellKnown).0 run(wellKnown) { result in
if let url = wellKnownResults.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }), 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), let href = WebURL(url.href),
href.host == WebURL(self.baseURL)?.host { href.host == WebURL(self.baseURL)?.host {
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path)) let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: href.path))
return try await run(nodeInfo).0 self.run(nodeInfo, completion: completion)
} else { }
throw NodeInfoError.noWellKnownLink }
} }
} }
@ -583,15 +600,4 @@ extension Client {
case invalidModel(Swift.Error) case invalidModel(Swift.Error)
case mastodonError(Int, String) 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 import Foundation
public enum SearchOperatorType: String, CaseIterable, Equatable, Sendable { public enum SearchOperatorType: String, CaseIterable, Equatable {
case has case has
case `is` case `is`
case language case language

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable { public struct UserAccountInfo: Equatable, Hashable, Identifiable {
public let id: String public let id: String
public let instanceURL: URL public let instanceURL: URL
public let clientID: String 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 { let image = UIImage(data: data) else {
return nil return nil
} }
#if os(visionOS)
let size: CGFloat = 50 * 2
#else
let size = 50 * UIScreen.main.scale let size = 50 * UIScreen.main.scale
#endif
return await image.byPreparingThumbnail(ofSize: CGSize(width: size, height: size)) ?? image return await image.byPreparingThumbnail(ofSize: CGSize(width: size, height: size)) ?? image
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -11,20 +11,18 @@ import Pachyderm
import Combine import Combine
import UserAccounts import UserAccounts
import InstanceFeatures import InstanceFeatures
#if canImport(Sentry)
import Sentry import Sentry
#endif
import ComposeUI import ComposeUI
import OSLog
private let oauthScopes = [Scope.read, .write, .follow] 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]() static private(set) var all = [UserAccountInfo: MastodonController]()
@available(*, message: "do something less dumb")
static var first: MastodonController { all.first!.value }
@MainActor @MainActor
static func getForAccount(_ account: UserAccountInfo) -> MastodonController { static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
if let controller = all[account] { if let controller = all[account] {
@ -36,12 +34,10 @@ final class MastodonController: ObservableObject, Sendable {
} }
} }
@MainActor
static func removeForAccount(_ account: UserAccountInfo) { static func removeForAccount(_ account: UserAccountInfo) {
all.removeValue(forKey: account) all.removeValue(forKey: account)
} }
@MainActor
static func resetAll() { static func resetAll() {
all = [:] all = [:]
} }
@ -50,31 +46,26 @@ final class MastodonController: ObservableObject, Sendable {
nonisolated let persistentContainer: MastodonCachePersistentStore// = MastodonCachePersistentStore(for: accountInfo, transient: transient) nonisolated let persistentContainer: MastodonCachePersistentStore// = MastodonCachePersistentStore(for: accountInfo, transient: transient)
let instanceURL: URL let instanceURL: URL
let accountInfo: UserAccountInfo? var accountInfo: UserAccountInfo?
@MainActor var accountPreferences: AccountPreferences!
private(set) var accountPreferences: AccountPreferences!
private(set) var client: Client! let client: Client!
let instanceFeatures = InstanceFeatures() let instanceFeatures = InstanceFeatures()
@MainActor @Published private(set) var account: AccountMO? @Published private(set) var account: AccountMO?
@MainActor @Published private(set) var instance: InstanceV1? @Published private(set) var instance: InstanceV1?
@MainActor @Published private var instanceV2: InstanceV2? @Published private var instanceV2: InstanceV2?
@MainActor @Published private(set) var instanceInfo: InstanceInfo! @Published private(set) var instanceInfo: InstanceInfo!
@MainActor @Published private(set) var nodeInfo: NodeInfo! @Published private(set) var nodeInfo: NodeInfo!
@MainActor @Published private(set) var lists: [List] = [] @Published private(set) var lists: [List] = []
@MainActor @Published private(set) var customEmojis: [Emoji]? @Published private(set) var customEmojis: [Emoji]?
@MainActor @Published private(set) var followedHashtags: [FollowedHashtag] = [] @Published private(set) var followedHashtags: [FollowedHashtag] = []
@MainActor @Published private(set) var filters: [FilterMO] = [] @Published private(set) var filters: [FilterMO] = []
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@MainActor private var pendingOwnInstanceRequestCallbacks = [(Result<InstanceV1, Client.Error>) -> Void]()
private var fetchOwnInstanceTask: Task<InstanceV1, any Error>? private var ownInstanceRequest: URLSessionTask?
@MainActor
private var fetchOwnAccountTask: Task<MainThreadBox<AccountMO>, any Error>?
@MainActor
private var fetchCustomEmojisTask: Task<[Emoji], Never>?
var loggedIn: Bool { var loggedIn: Bool {
accountInfo != nil accountInfo != nil
@ -106,7 +97,6 @@ final class MastodonController: ObservableObject, Sendable {
} }
.store(in: &cancellables) .store(in: &cancellables)
#if canImport(Sentry)
$instanceInfo $instanceInfo
.compactMap { $0 } .compactMap { $0 }
.removeDuplicates(by: { $0.version == $1.version }) .removeDuplicates(by: { $0.version == $1.version })
@ -115,7 +105,6 @@ final class MastodonController: ObservableObject, Sendable {
setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo) setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo)
} }
.store(in: &cancellables) .store(in: &cancellables)
#endif
$instance $instance
.compactMap { $0 } .compactMap { $0 }
@ -229,36 +218,21 @@ final class MastodonController: ObservableObject, Sendable {
Task { Task {
do { do {
_ = try await getOwnAccount() async let ownAccount = try getOwnAccount()
} catch { async let ownInstance = try getOwnInstance()
logger.error("Fetch own account failed: \(String(describing: error))")
} _ = try await (ownAccount, ownInstance)
}
let instanceTask = Task {
do {
_ = try await getOwnInstance()
if instanceFeatures.hasMastodonVersion(4, 0, 0) { if instanceFeatures.hasMastodonVersion(4, 0, 0) {
_ = try? await getOwnInstanceV2() async let _ = try? getOwnInstanceV2()
}
} catch {
logger.error("Fetch instance failed: \(String(describing: error))")
}
} }
Task { loadLists()
_ = await instanceTask.value
await loadLists()
}
Task {
_ = await instanceTask.value
_ = await loadFilters() _ = await loadFilters()
}
Task {
_ = await instanceTask.value
await loadServerPreferences() await loadServerPreferences()
} catch {
Logging.general.error("MastodonController initialization failed: \(String(describing: error))")
}
} }
} }
@ -272,19 +246,19 @@ final class MastodonController: ObservableObject, Sendable {
} }
} }
@MainActor func getOwnAccount(completion: ((Result<AccountMO, Client.Error>) -> Void)? = nil) {
func getOwnAccount() async throws -> AccountMO {
if let account { if let account {
return account completion?(.success(account))
} else if let fetchOwnAccountTask {
return try await fetchOwnAccountTask.value.value
} else { } else {
let task = Task { let request = Client.getSelfAccount()
let account = try await run(Client.getSelfAccount()).0 run(request) { response in
switch response {
case let .failure(error):
completion?(.failure(error))
let context = persistentContainer.viewContext case let .success(account, _):
// this closure is declared separately so we can tell the compiler it's Sendable let context = self.persistentContainer.viewContext
let performBlock: @MainActor @Sendable () -> MainThreadBox<AccountMO> = { context.perform {
let accountMO: AccountMO let accountMO: AccountMO
if let existing = self.persistentContainer.account(for: account.id, in: context) { if let existing = self.persistentContainer.account(for: account.id, in: context) {
accountMO = existing accountMO = existing
@ -292,73 +266,111 @@ final class MastodonController: ObservableObject, Sendable {
} else { } else {
accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context) accountMO = self.persistentContainer.addOrUpdateSynchronously(account: account, in: context)
} }
// TODO: is AccountMO.active used anywhere?
accountMO.active = true accountMO.active = true
self.account = accountMO self.account = accountMO
return MainThreadBox(value: accountMO) completion?(.success(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) { func getOwnAccount() async throws -> AccountMO {
Task.detached { if let account = account {
let ownInstance = try? await self.getOwnInstance() return account
if let ownInstance { } else {
completion?(ownInstance) 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 @MainActor
func getOwnInstance() async throws -> InstanceV1 { func getOwnInstance() async throws -> InstanceV1 {
if let instance { return try await withCheckedThrowingContinuation({ continuation in
return instance getOwnInstanceInternal(retryAttempt: 0) { result in
} else if let fetchOwnInstanceTask { continuation.resume(with: result)
return try await fetchOwnInstanceTask.value
} else {
let task = Task {
let instance = try await retrying("Fetch Own Instance") {
try await run(Client.getInstanceV1()).0
} }
})
}
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 {
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 self.instance = instance
Task { for completion in self.pendingOwnInstanceRequestCallbacks {
await fetchNodeInfo() completion(.success(instance))
} }
self.pendingOwnInstanceRequestCallbacks = []
return instance
} }
fetchOwnInstanceTask = task
return try await task.value
} }
} }
@MainActor client.nodeInfo { result in
private func fetchNodeInfo() async { switch result {
if let nodeInfo = try? await client.nodeInfo() { case let .failure(error):
print("Unable to get node info: \(error)")
case let .success(nodeInfo, _):
DispatchQueue.main.async {
self.nodeInfo = 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)
} }
} }
return try await action() }
} }
@MainActor
private func getOwnInstanceV2() async throws { private func getOwnInstanceV2() async throws {
self.instanceV2 = try await client.run(Client.getInstanceV2()).0 self.instanceV2 = try await client.run(Client.getInstanceV2()).0
} }
@ -409,35 +421,33 @@ final class MastodonController: ObservableObject, Sendable {
} }
} }
@MainActor func getCustomEmojis(completion: @escaping ([Emoji]) -> Void) {
func getCustomEmojis() async -> [Emoji] { if let emojis = self.customEmojis {
if let customEmojis { completion(emojis)
return customEmojis
} else if let fetchCustomEmojisTask {
return await fetchCustomEmojisTask.value
} else { } else {
let task = Task { let request = Client.getCustomEmoji()
let emojis = (try? await run(Client.getCustomEmoji()).0) ?? [] run(request) { (response) in
customEmojis = emojis if case let .success(emojis, _) = response {
return emojis 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() let req = Client.getLists()
guard let (lists, _) = try? await run(req) else { run(req) { response in
return if case .success(let lists, _) = response {
DispatchQueue.main.async {
self.lists = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
} }
let sorted = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title)) let context = self.persistentContainer.backgroundContext
await MainActor.run { context.perform {
self.lists = sorted
}
let context = persistentContainer.backgroundContext
await context.perform {
for list in lists { for list in lists {
if let existing = try? context.fetch(ListMO.fetchRequest(id: list.id)).first { if let existing = try? context.fetch(ListMO.fetchRequest(id: list.id)).first {
existing.updateFrom(apiList: list) existing.updateFrom(apiList: list)
@ -448,6 +458,8 @@ final class MastodonController: ObservableObject, Sendable {
self.persistentContainer.save(context: context) self.persistentContainer.save(context: context)
} }
} }
}
}
private func loadCachedLists() -> [List] { private func loadCachedLists() -> [List] {
let req = ListMO.fetchRequest() let req = ListMO.fetchRequest()
@ -532,7 +544,6 @@ final class MastodonController: ObservableObject, Sendable {
filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? [] filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? []
} }
@MainActor
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft { func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft {
var acctsToMention = [String]() var acctsToMention = [String]()
@ -585,7 +596,6 @@ final class MastodonController: ObservableObject, Sendable {
) )
} }
@MainActor
func createDraft(editing status: StatusMO, source: StatusSource) -> Draft { func createDraft(editing status: StatusMO, source: StatusSource) -> Draft {
precondition(status.id == source.id) precondition(status.id == source.id)
let draft = DraftsPersistentContainer.shared.createEditDraft( let draft = DraftsPersistentContainer.shared.createEditDraft(
@ -602,7 +612,6 @@ final class MastodonController: ObservableObject, Sendable {
} }
#if canImport(Sentry)
private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?) { private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?) {
let crumb = Breadcrumb(level: .info, category: "MastodonController") let crumb = Breadcrumb(level: .info, category: "MastodonController")
crumb.data = [ crumb.data = [
@ -618,4 +627,3 @@ private func setInstanceBreadcrumb(instance: InstanceInfo, nodeInfo: NodeInfo?)
} }
SentrySDK.addBreadcrumb(crumb) SentrySDK.addBreadcrumb(crumb)
} }
#endif

View File

@ -80,11 +80,9 @@ class ReblogService {
status.reblogged.toggle() status.reblogged.toggle()
mastodonController.persistentContainer.statusSubject.send(status.id) mastodonController.persistentContainer.statusSubject.send(status.id)
#if !os(visionOS)
if hapticFeedback { if hapticFeedback {
UIImpactFeedbackGenerator(style: .light).impactOccurred() UIImpactFeedbackGenerator(style: .light).impactOccurred()
} }
#endif
let request: Request<Status> let request: Request<Status>
if status.reblogged { if status.reblogged {
@ -106,11 +104,9 @@ class ReblogService {
} }
presenter.showToast(configuration: config, animated: true) presenter.showToast(configuration: config, animated: true)
#if !os(visionOS)
if hapticFeedback { if hapticFeedback {
UINotificationFeedbackGenerator().notificationOccurred(.error) 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" return "Save to Photos"
} }
override var activityImage: UIImage? { override var activityImage: UIImage? {
// Just using the symbol image directly causes it to be stretched. UIImage(systemName: "square.and.arrow.down")
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)
}
} }
override func canPerform(withActivityItems activityItems: [Any]) -> Bool { override func canPerform(withActivityItems activityItems: [Any]) -> Bool {

View File

@ -33,7 +33,7 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
let metadata = LPLinkMetadata() let metadata = LPLinkMetadata()
metadata.originalURL = status.url! metadata.originalURL = status.url!
metadata.url = 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) let content = converter.convert(html: status.content)
metadata.title = "\(status.account.displayName): \"\(content)\"" metadata.title = "\(status.account.displayName): \"\(content)\""
if let avatar = status.account.avatar, 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 UIKit
import CoreData import CoreData
import OSLog import OSLog
#if canImport(Sentry)
import Sentry import Sentry
#endif
import UserAccounts import UserAccounts
import ComposeUI import ComposeUI
import TuskerPreferences import TuskerPreferences
@ -21,17 +19,13 @@ typealias Preferences = TuskerPreferences.Preferences
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration") let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate")
@main @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
#if canImport(Sentry)
configureSentry() configureSentry()
#endif
#if !os(visionOS)
swizzleStatusBar() swizzleStatusBar()
swizzlePresentationController() swizzlePresentationController()
#endif
AppShortcutItem.createItems(for: application) AppShortcutItem.createItems(for: application)
@ -62,9 +56,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let oldPreferencesFile = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist") let oldPreferencesFile = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
if FileManager.default.fileExists(atPath: oldPreferencesFile.path) { if FileManager.default.fileExists(atPath: oldPreferencesFile.path) {
if case .failure(let error) = Preferences.migrate(from: oldPreferencesFile) { if case .failure(let error) = Preferences.migrate(from: oldPreferencesFile) {
#if canImport(Sentry)
SentrySDK.capture(error: error) 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) { for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) {
DraftsPersistentContainer.shared.migrate(from: url) { DraftsPersistentContainer.shared.migrate(from: url) {
if case .failure(let error) = $0 { if case .failure(let error) = $0 {
#if canImport(Sentry)
SentrySDK.capture(error: error) SentrySDK.capture(error: error)
#endif
} }
} }
} }
@ -91,7 +81,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true return true
} }
#if canImport(Sentry)
private func configureSentry() { private func configureSentry() {
guard let dsn = Bundle.main.object(forInfoDictionaryKey: "SentryDSN") as? String, guard let dsn = Bundle.main.object(forInfoDictionaryKey: "SentryDSN") as? String,
!dsn.isEmpty else { !dsn.isEmpty else {
@ -123,17 +112,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
event.context?.removeValue(forKey: "culture") event.context?.removeValue(forKey: "culture")
return Preferences.shared.reportErrorsAutomatically ? event : nil return Preferences.shared.reportErrorsAutomatically ? event : nil
} }
}
if let clazz = NSClassFromString("SentryInstallation"), if let clazz = NSClassFromString("SentryInstallation"),
let objClazz = clazz as AnyObject as? NSObject, let objClazz = clazz as AnyObject as? NSObject,
let id = objClazz.perform(Selector(("idWithCacheDirectoryPath:")), with: options.cacheDirectoryPath).takeUnretainedValue() as? String { let id = objClazz.value(forKey: "id") as? String {
logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)") logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)")
} }
} }
}
#endif
override func buildMenu(with builder: UIMenuBuilder) { override func buildMenu(with builder: UIMenuBuilder) {
if builder.system == .main { if builder.system == .main {
MenuController.buildMainMenu(builder: builder) MenuController.buildMainMenu(builder: builder)
} }
@ -173,7 +162,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
} }
#if !os(visionOS)
private func swizzleStatusBar() { private func swizzleStatusBar() {
let selector = Selector(("handleTapAction:")) let selector = Selector(("handleTapAction:"))
var originalIMP: IMP? var originalIMP: IMP?
@ -225,6 +213,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Logging.general.error("Unable to swizzle presentation controller") 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 import UIKit
#if os(visionOS) class ImageCache {
private let imageScale: CGFloat = 2
#else
@MainActor
private let imageScale = UIScreen.main.scale
#endif
final class ImageCache: @unchecked Sendable {
@MainActor
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24 * 7), desiredSize: CGSize(width: 50, height: 50)) 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)) 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)) 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)) static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
#if DEBUG #if DEBUG
@ -35,15 +24,14 @@ final class ImageCache: @unchecked Sendable {
private let cache: ImageDataCache private let cache: ImageDataCache
private let desiredPixelSize: CGSize? private let desiredPixelSize: CGSize?
@MainActor
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) { 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? // 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.desiredPixelSize = pixelSize
self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, 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, if !ImageCache.disableCaching,
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) { let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
completion?(entry.data, entry.image) 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) { return Task.detached(priority: .userInitiated) {
let result = await self.fetch(url: url) let result = await self.fetch(url: url)
switch result { switch result {

View File

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

View File

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

View File

@ -8,12 +8,23 @@
import Foundation import Foundation
private let ASCII_NEWLINE: unichar = 10
private let ASCII_SPACE: unichar = 32
extension NSAttributedString { extension NSAttributedString {
var fullRange: NSRange { var fullRange: NSRange {
return NSRange(location: 0, length: self.length) 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 { 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 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 Copyright (c) 2022, Chime
All rights reserved. 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. /// 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. /// It will crash if run on any non-main thread.
@_unavailableFromAsync @MainActor(unsafe)
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T { 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)) dispatchPrecondition(condition: .onQueue(.main))
return try withoutActuallyEscaping(body) { fn in
try unsafeBitCast(fn, to: (() throws -> T).self)()
}
}
@_unavailableFromAsync return try body()
@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)()
}
} }
} }

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 import Combine
extension View { extension View {
@MainActor
@ViewBuilder @ViewBuilder
func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View { func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {

View File

@ -23,7 +23,7 @@ class HTMLConverter {
return style return style
}() }()
private let converter: AttributedStringConverter<Callbacks> private var converter: AttributedStringConverter<Callbacks>
init(font: UIFont, monospaceFont: UIFont, color: UIColor, paragraphStyle: NSParagraphStyle) { init(font: UIFont, monospaceFont: UIFont, color: UIColor, paragraphStyle: NSParagraphStyle) {
let config = AttributedStringConverterConfiguration(font: font, monospaceFont: monospaceFont, color: color, paragraphStyle: paragraphStyle) 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>() private static let cache = NSCache<NSURL, UIImage>()
static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? { static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? {
let grayscale = MainActor.runUnsafelyMaybeIntroducingDataRace { if Preferences.shared.grayscaleImages,
Preferences.shared.grayscaleImages
}
if grayscale,
let source = image.cgImage { let source = image.cgImage {
// todo: should this return the original image if conversion fails? // todo: should this return the original image if conversion fails?
return convert(url: url, cgImage: source) 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? { static func convert(url: URL?, image: UIImage) -> UIImage? {
if let url, if let url,
let cached = cache.object(forKey: url as NSURL) { let cached = cache.object(forKey: url as NSURL) {
@ -50,21 +35,6 @@ struct ImageGrayscalifier {
return doConvert(CIImage(cgImage: cgImage), url: url) 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? { static func convert(url: URL?, data: Data) -> UIImage? {
if let url = url, if let url = url,
let cached = cache.object(forKey: url as NSURL) { let cached = cache.object(forKey: url as NSURL) {
@ -86,18 +56,6 @@ struct ImageGrayscalifier {
return doConvert(CIImage(cgImage: cgImage), url: url) 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? { private static func doConvert(_ source: CIImage, url: URL?) -> UIImage? {
guard let filter = CIFilter(name: "CIColorMonochrome") else { guard let filter = CIFilter(name: "CIColorMonochrome") else {
return nil 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 import UIKit
@MainActor
struct MenuController { struct MenuController {
static let composeCommand: UIKeyCommand = { 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]> // once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]>
// to make the lock semantics more clear // to make the lock semantics more clear
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
@available(visionOS 1.0, *) class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
final class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
#if os(visionOS)
private let lock = OSAllocatedUnfairLock(initialState: [Key: Value]())
#else
private let lock: any Lock<[Key: Value]> private let lock: any Lock<[Key: Value]>
#endif
init() { init() {
#if !os(visionOS)
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
self.lock = OSAllocatedUnfairLock(initialState: [:]) self.lock = OSAllocatedUnfairLock(initialState: [:])
} else { } else {
self.lock = UnfairLock(initialState: [:]) self.lock = UnfairLock(initialState: [:])
} }
#endif
} }
subscript(key: Key) -> Value? { subscript(key: Key) -> Value? {
@ -37,15 +30,9 @@ final class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable>: @u
} }
} }
set(value) { set(value) {
#if os(visionOS)
lock.withLock { dict in
dict[key] = value
}
#else
_ = lock.withLock { dict in _ = lock.withLock { dict in
dict[key] = value 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 // TODO: replace this only with OSAllocatedUnfairLock
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
fileprivate protocol Lock<State> { fileprivate protocol Lock<State> {
@ -101,4 +87,3 @@ fileprivate class UnfairLock<State>: Lock {
return try body(&state) return try body(&state)
} }
} }
#endif

View File

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

View File

@ -11,7 +11,6 @@ import Pachyderm
import TuskerPreferences import TuskerPreferences
extension StatusSwipeAction { extension StatusSwipeAction {
@MainActor
func createAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { func createAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
switch self { switch self {
case .reply: case .reply:
@ -30,7 +29,6 @@ extension StatusSwipeAction {
} }
} }
@MainActor
protocol StatusSwipeActionContainer: UIView { protocol StatusSwipeActionContainer: UIView {
var mastodonController: MastodonController! { get } var mastodonController: MastodonController! { get }
var navigationDelegate: any TuskerNavigationDelegate { get } var navigationDelegate: any TuskerNavigationDelegate { get }
@ -42,7 +40,6 @@ protocol StatusSwipeActionContainer: UIView {
func performReplyAction() func performReplyAction()
} }
@MainActor
private func createReplyAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { private func createReplyAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else { guard container.mastodonController.loggedIn else {
return nil return nil
@ -56,7 +53,6 @@ private func createReplyAction(status: StatusMO, container: StatusSwipeActionCon
return action return action
} }
@MainActor
private func createFavoriteAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { private func createFavoriteAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else { guard container.mastodonController.loggedIn else {
return nil return nil
@ -73,7 +69,6 @@ private func createFavoriteAction(status: StatusMO, container: StatusSwipeAction
return action return action
} }
@MainActor
private func createReblogAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { private func createReblogAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn, guard container.mastodonController.loggedIn,
container.canReblog else { container.canReblog else {
@ -91,7 +86,6 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
return action return action
} }
@MainActor
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
MainActor.runUnsafely { MainActor.runUnsafely {
@ -106,7 +100,6 @@ private func createShareAction(status: StatusMO, container: StatusSwipeActionCon
return action return action
} }
@MainActor
private func createBookmarkAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { private func createBookmarkAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else { guard container.mastodonController.loggedIn else {
return nil return nil
@ -131,10 +124,11 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction
return action return action
} }
@MainActor
private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in
MainActor.runUnsafely {
container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false) container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false)
}
completion(true) completion(true)
} }
action.image = UIImage(systemName: "safari") 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 Pachyderm
import CoreData import CoreData
// TODO: remove this class eventually
class SavedDataManager: Codable { class SavedDataManager: Codable {
@MainActor
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
@MainActor
private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist") private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
@MainActor
static func load() -> SavedDataManager? { static func load() -> SavedDataManager? {
let decoder = PropertyListDecoder() let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: archiveURL), if let data = try? Data(contentsOf: archiveURL),
@ -27,7 +23,6 @@ class SavedDataManager: Codable {
return nil return nil
} }
@MainActor
static func destroy() throws { static func destroy() throws {
try FileManager.default.removeItem(at: archiveURL) try FileManager.default.removeItem(at: archiveURL)
} }
@ -44,7 +39,6 @@ class SavedDataManager: Codable {
return s return s
} }
@MainActor
private func save() { private func save() {
let encoder = PropertyListEncoder() let encoder = PropertyListEncoder()
let data = try? encoder.encode(self) let data = try? encoder.encode(self)
@ -52,6 +46,8 @@ class SavedDataManager: Codable {
} }
func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws { func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws {
var changed = false
if let hashtags = savedHashtags[accountID] { if let hashtags = savedHashtags[accountID] {
let objects: [[String: Any]] = hashtags.map { let objects: [[String: Any]] = hashtags.map {
["url": $0.url, "name": $0.name] ["url": $0.url, "name": $0.name]
@ -59,6 +55,7 @@ class SavedDataManager: Codable {
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects) let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)
try context.execute(hashtagsReq) try context.execute(hashtagsReq)
savedHashtags.removeValue(forKey: accountID) savedHashtags.removeValue(forKey: accountID)
changed = true
} }
if let instances = savedInstances[accountID] { if let instances = savedInstances[accountID] {
@ -68,6 +65,11 @@ class SavedDataManager: Codable {
let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects) let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects)
try context.execute(instancesReq) try context.execute(instancesReq)
savedInstances.removeValue(forKey: accountID) savedInstances.removeValue(forKey: accountID)
changed = true
}
if changed {
save()
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -13,11 +13,11 @@ class AccountFollowsViewController: SegmentedPageViewController<AccountFollowsVi
let accountID: String let accountID: String
let mastodonController: MastodonController let mastodonController: MastodonController
init(accountID: String, initialPage: Mode = .following, mastodonController: MastodonController) { init(accountID: String, mastodonController: MastodonController) {
self.accountID = accountID self.accountID = accountID
self.mastodonController = mastodonController 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) 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 UIKit
import PencilKit import PencilKit
@MainActor
protocol ComposeDrawingViewControllerDelegate: AnyObject { protocol ComposeDrawingViewControllerDelegate: AnyObject {
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)

View File

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

View File

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

View File

@ -296,7 +296,7 @@ extension ConversationCollectionViewController {
case mainStatus case mainStatus
case childThread(firstStatusID: String) case childThread(firstStatusID: String)
} }
enum Item: Hashable, Sendable { enum Item: Hashable {
case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool) case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool)
case expandThread(childThreads: [ConversationNode], inline: Bool) case expandThread(childThreads: [ConversationNode], inline: Bool)
case loadingIndicator 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)): 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 return a == b && aPrev == bPrev && aNext == bNext
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)): 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): case (.loadingIndicator, .loadingIndicator):
return true return true
default: default:
@ -324,7 +324,7 @@ extension ConversationCollectionViewController {
case .expandThread(childThreads: let childThreads, inline: let inline): case .expandThread(childThreads: let childThreads, inline: let inline):
hasher.combine(1) hasher.combine(1)
for thread in childThreads { for thread in childThreads {
hasher.combine(thread.statusID) hasher.combine(thread.status.id)
} }
hasher.combine(inline) hasher.combine(inline)
case .loadingIndicator: case .loadingIndicator:
@ -413,12 +413,6 @@ extension ConversationCollectionViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self) 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 { extension ConversationCollectionViewController: UICollectionViewDragDelegate {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,11 +20,8 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
var account: Account? var account: Account?
private var accountImagesTask: Task<Void, Never>? private var avatarRequest: ImageCache.Request?
private var headerRequest: ImageCache.Request?
deinit {
accountImagesTask?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -65,35 +62,37 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
noteTextView.setEmojis(account.emojis, identifier: account.id) noteTextView.setEmojis(account.emojis, identifier: account.id)
avatarImageView.image = nil avatarImageView.image = nil
headerImageView.image = nil if let avatar = account.avatar {
avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
accountImagesTask?.cancel() defer {
accountImagesTask = Task { self?.avatarRequest = nil
await updateImages(account: account)
} }
} guard let self = self,
let image = image,
private nonisolated func updateImages(account: Account) async { self.account?.id == account.id else {
await withTaskGroup(of: Void.self) { group in
group.addTask {
guard let avatar = account.avatar,
let image = await ImageCache.avatars.get(avatar).1 else {
return return
} }
await MainActor.run { DispatchQueue.main.async {
self.avatarImageView.image = image 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 return
} }
await MainActor.run { DispatchQueue.main.async {
self.headerImageView.image = image 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?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection) super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors() updateLayerColors()
} }
#endif
override func layoutSubviews() { override func layoutSubviews() {
super.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. // Copyright © 2023 Shadowfacts. All rights reserved.
// //
#if !os(visionOS)
import UIKit import UIKit
import Pachyderm import Pachyderm
import SwiftUI import SwiftUI
@ -99,13 +98,10 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
} }
// Unneeded on visionOS since there is no light/dark mode
#if !os(visionOS)
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection) super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors() updateLayerColors()
} }
#endif
private func updateLayerColors() { private func updateLayerColors() {
if traitCollection.userInterfaceStyle == .dark { if traitCollection.userInterfaceStyle == .dark {
@ -128,12 +124,10 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact { if traitCollection.horizontalSizeClass == .compact || traitCollection.verticalSizeClass == .compact {
toPresent = UINavigationController(rootViewController: host) toPresent = UINavigationController(rootViewController: host)
toPresent.modalPresentationStyle = .pageSheet toPresent.modalPresentationStyle = .pageSheet
#if !os(visionOS)
let sheetPresentationController = toPresent.sheetPresentationController! let sheetPresentationController = toPresent.sheetPresentationController!
sheetPresentationController.detents = [ sheetPresentationController.detents = [
.medium() .medium()
] ]
#endif
} else { } else {
host.modalPresentationStyle = .popover host.modalPresentationStyle = .popover
let popoverPresentationController = host.popoverPresentationController! 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 UIKit
import Pachyderm import Pachyderm
#if os(visionOS)
import SwiftUI
#endif
class SuggestedProfilesViewController: UIViewController, CollectionViewController { class SuggestedProfilesViewController: UIViewController, CollectionViewController {
private let mastodonController: MastodonController weak var mastodonController: MastodonController!
var collectionView: UICollectionView! var collectionView: UICollectionView!
private var layout: MultiColumnCollectionViewLayout! private var layout: MultiColumnCollectionViewLayout!
@ -56,22 +53,10 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { 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 let accountCell = UICollectionView.CellRegistration<SuggestedProfileCardCollectionViewCell, (String, Suggestion.Source)>(cellNib: UINib(nibName: "SuggestedProfileCardCollectionViewCell", bundle: .main)) { cell, indexPath, item in
cell.delegate = self cell.delegate = self
cell.updateUI(accountID: item.0, source: item.1) cell.updateUI(accountID: item.0, source: item.1)
} }
#endif
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier { switch itemIdentifier {
case .account(let id, let source): case .account(let id, let source):
@ -110,9 +95,7 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
await MainActor.run { await dataSource.apply(snapshot)
dataSource.apply(snapshot)
}
do { do {
let request = Client.getSuggestions(limit: 80) let request = Client.getSuggestions(limit: 80)
@ -125,9 +108,7 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) }) snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) })
await MainActor.run { await dataSource.apply(snapshot)
dataSource.apply(snapshot)
}
state = .loaded state = .loaded
} catch { } catch {

View File

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

View File

@ -6,7 +6,6 @@
// Copyright © 2022 Shadowfacts. All rights reserved. // Copyright © 2022 Shadowfacts. All rights reserved.
// //
#if !os(visionOS)
import UIKit import UIKit
import Pachyderm import Pachyderm
import WebURLFoundationExtras import WebURLFoundationExtras
@ -79,7 +78,7 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines) let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
providerLabel.text = provider 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.text = converter.convert(html: card.description)
descriptionLabel.isHidden = description.isEmpty 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?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection) super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors() updateLayerColors()
} }
#endif
private func updateLayerColors() { private func updateLayerColors() {
if traitCollection.userInterfaceStyle == .dark { if traitCollection.userInterfaceStyle == .dark {
@ -173,4 +169,3 @@ extension TrendingLinkCardCollectionViewCell {
case regular, compact case regular, compact
} }
} }
#endif

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