Compare commits
No commits in common. "ec767542705075b34977ecc8c6ac208998b784f4" and "5a4323067ad63772fb9052a4e3aebe69d7305be3" have entirely different histories.
ec76754270
...
5a4323067a
|
@ -1,37 +1,3 @@
|
||||||
## 2024.1
|
|
||||||
This update includes a significant improvements for the attachment gallery and displaying rich text posts. See below for a full list of improvements and fixes.
|
|
||||||
|
|
||||||
Features/Improvements:
|
|
||||||
- Improve attachment gallery
|
|
||||||
- Improve animations
|
|
||||||
- Display video captions
|
|
||||||
- Support sharing/saving videos
|
|
||||||
- Resume music playback after playing videos
|
|
||||||
- Improve rich text display in posts
|
|
||||||
- Add See Results button to polls
|
|
||||||
- Add Share and Save to Photos menu items to post attachments
|
|
||||||
- Show verified links in account lists
|
|
||||||
- Display message on empty list timelines
|
|
||||||
- Add preference to indicate attachments lacking alt text
|
|
||||||
- Mark notifications as read on Mastodon web frontend once displayed
|
|
||||||
- iPadOS: Support tapping the selected sidebar item to scroll to top
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix issue changing scope after searching
|
|
||||||
- Fix crash when searching "from:me"
|
|
||||||
- Fix tapping Followers button on profile opening Following screen
|
|
||||||
- Fix crash when removing poll option on Compose screen
|
|
||||||
- Fix hang when sharing video/GIFV attachments
|
|
||||||
- Fix stretched Save to Photos icon when sharing attachments
|
|
||||||
- Fix GIFV playback preventing device sleep
|
|
||||||
- Fix Notifications tab not scrolling to top when tab bar item tapped
|
|
||||||
- Fix selection not clearing on Trending Hashtags
|
|
||||||
- Fix fast account switcher overlapping iPhone sensor housing in landscape
|
|
||||||
- Fix Edit List screen not updating when adding/removing accounts
|
|
||||||
- Fix changing list reply policy not refreshing timeline
|
|
||||||
- Pixelfed: Fix crash when there are multiple follow notifications from the same account
|
|
||||||
- macOS: Fix attachment gallery displaying improperly when Reduce Motion is on
|
|
||||||
|
|
||||||
## 2023.8
|
## 2023.8
|
||||||
This update adds support for search operators and post translation, and improves support for displaying rich-text posts. See below for a full list of improvements and fixes.
|
This update adds support for search operators and post translation, and improves support for displaying rich-text posts. See below for a full list of improvements and fixes.
|
||||||
|
|
||||||
|
|
72
CHANGELOG.md
72
CHANGELOG.md
|
@ -1,77 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2024.1 (119)
|
|
||||||
Features/Improvements:
|
|
||||||
- Add Account Settings button to Preferences
|
|
||||||
|
|
||||||
## 2024.1 (118)
|
|
||||||
Bugfixes:
|
|
||||||
- Fix music not pausing/resuming when video playback starts
|
|
||||||
|
|
||||||
## 2024.1 (117)
|
|
||||||
Features/Improvements:
|
|
||||||
- Add See Results button to polls
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix race condition when presenting gallery for 4th of more than 4 attachments
|
|
||||||
- Fix gallery interactive dismissal not working for 4th or later attachments on posts with more than 4 attachments
|
|
||||||
- Pixelfed: Fix crash when there are multiple follow notifications from the same account
|
|
||||||
- macOS: Fix gallery being positioned incorrectly when Reduce Motion is on
|
|
||||||
|
|
||||||
## 2024.1 (116)
|
|
||||||
Features/Improvements:
|
|
||||||
- Display message on empty list timelines
|
|
||||||
- Add preference to display badge for attachments that lack alt text
|
|
||||||
- Mark notifications as read on the Mastodon web frontend once displayed
|
|
||||||
- iPadOS: Support tapping the selected sidebar item to scroll to top
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix playing back GIFVs preventing the device sleeping
|
|
||||||
- Fix incorrect cell separator insets followers/following lists
|
|
||||||
- Fix memory leak in attachments gallery
|
|
||||||
- Fix notifications tab not scrolling to top when tab bar item tapped
|
|
||||||
- Fix Trending Hashtags screen not clearing selection
|
|
||||||
- Fix fast account switcher overlapping sensor housing on landscape iPhones
|
|
||||||
- Fix Edit List screen not updating when accounts are added/removed
|
|
||||||
- Fix changing List reply policy not refreshing list timeline
|
|
||||||
- macOS: Fix certain gallery attachments being incorrectly sized/positioned
|
|
||||||
|
|
||||||
## 2024.1 (115)
|
|
||||||
Features/Improvements:
|
|
||||||
- Rewrite attachment gallery
|
|
||||||
- Fixes a number of long-standing issues
|
|
||||||
- Adds a custom video player that shows controls and caption
|
|
||||||
- Supports sharing/saving videos
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix hang when sharing video/gifv attachments
|
|
||||||
- Fix stretched icon for Save to Photos action when sharing attachment
|
|
||||||
- Fix crash when Compose screen is dismissed while adding attachments
|
|
||||||
- Fix crash when sharing attachment from context menu on iPad
|
|
||||||
|
|
||||||
## 2024.1 (113)
|
|
||||||
Features/Improvements:
|
|
||||||
- Add Share and Save to Photos context menu actions to attachments
|
|
||||||
- Show verified link in account lists
|
|
||||||
- Change cell separator appearance on posts
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix tapping Followers button on profiles opening Following screen
|
|
||||||
- Fix crash when removing poll option on Compose screen
|
|
||||||
- Fix leading indentation in post text being ignored
|
|
||||||
- Fix crash when viewing posts containing HTML numeric character references
|
|
||||||
- Fix paragraphs starting with links being combined with previous paragraph
|
|
||||||
|
|
||||||
## 2024.1 (112)
|
|
||||||
Bugfixes:
|
|
||||||
- Fix profile field links not displaying
|
|
||||||
- Fix various issues displaying rich text in posts
|
|
||||||
- Fix issue changing scope after searching
|
|
||||||
- Fix crash when searching for "from:me"
|
|
||||||
|
|
||||||
## 2024.1 (111)
|
|
||||||
This build contains a complete rewrite of the HTML parsing pipeline for displaying posts. If you notice any issues with how post text appears—especially when it differs from on the web—please report it!
|
|
||||||
|
|
||||||
## 2023.8 (110)
|
## 2023.8 (110)
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
- Fix potential crash after deleting List on Explore screen
|
- Fix potential crash after deleting List on Explore screen
|
||||||
|
|
|
@ -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)
|
||||||
|
|
7
Packages/ComposeUI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
Packages/ComposeUI/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
|
@ -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]
|
||||||
|
|
|
@ -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, *) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public protocol DuckableViewController: UIViewController {
|
public protocol DuckableViewController: UIViewController {
|
||||||
func duckableViewControllerShouldDuck() -> DuckAttemptAction
|
func duckableViewControllerShouldDuck() -> DuckAttemptAction
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
.DS_Store
|
|
||||||
/.build
|
|
||||||
/Packages
|
|
||||||
xcuserdata/
|
|
||||||
DerivedData/
|
|
||||||
.swiftpm/configuration/registries.json
|
|
||||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
|
||||||
.netrc
|
|
|
@ -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"]),
|
|
||||||
]
|
|
||||||
)
|
|
|
@ -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() {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
//
|
|
||||||
// GalleryContentViewControllerContainer.swift
|
|
||||||
// GalleryVC
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 12/28/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public protocol GalleryContentViewControllerContainer: AnyObject {
|
|
||||||
var galleryControlsVisible: Bool { get }
|
|
||||||
|
|
||||||
func setGalleryContentLoading(_ loading: Bool)
|
|
||||||
func galleryContentChanged()
|
|
||||||
func disableGalleryScrollAndZoom()
|
|
||||||
func setGalleryControlsVisible(_ visible: Bool, animated: Bool)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,133 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
to.view.frame = container.bounds
|
|
||||||
from.view.frame = container.bounds
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
toVC.view.frame = transitionContext.containerView.bounds
|
|
||||||
fromVC.view.frame = transitionContext.containerView.bounds
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
//
|
|
||||||
// GalleryDismissInteraction.swift
|
|
||||||
// GalleryVC
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 3/1/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
class GalleryDismissInteraction: NSObject {
|
|
||||||
|
|
||||||
private unowned 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,566 +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
|
|
||||||
|
|
||||||
private var scrollViewSizeForLastZoomScaleUpdate: CGSize?
|
|
||||||
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()
|
|
||||||
|
|
||||||
// When the scrollView size changes, make sure the zoom scale is up-to-date since it depends on the scrollView's bounds.
|
|
||||||
// This might also fix an issue on macOS (Designed for iPad) where the content isn't placed correctly. See #446
|
|
||||||
if scrollViewSizeForLastZoomScaleUpdate != scrollView.bounds.size {
|
|
||||||
scrollViewSizeForLastZoomScaleUpdate = scrollView.bounds.size
|
|
||||||
updateZoomScale(resetZoom: true)
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
scrollView.contentSize = content.contentSize
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +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)
|
|
||||||
}
|
|
||||||
|
|
||||||
animator.startAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
||||||
guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
to.view.alpha = 0
|
|
||||||
to.view.frame = transitionContext.containerView.bounds
|
|
||||||
transitionContext.containerView.addSubview(to.view)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
animator.startAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,179 +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 viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
|
|
||||||
if animated {
|
|
||||||
// Wait until the transition is no longer in-progress, otherwise things will just get deferred again.
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.presentationAnimationCompleted()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>()
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -7,53 +7,26 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct TimelineMarkers {
|
public struct TimelineMarkers: Decodable, Sendable {
|
||||||
private init() {}
|
public let home: Marker?
|
||||||
|
public let notifications: Marker?
|
||||||
|
|
||||||
public static func request<T: TimelineMarkerType>(timeline: T) -> Request<TimelineMarker<T.Payload>> {
|
public static func request(timelines: [Timeline]) -> Request<TimelineMarkers> {
|
||||||
Request(method: .get, path: "/api/v1/markers", queryParameters: ["timeline[]" => T.name])
|
return Request(method: .get, path: "/api/v1/markers", queryParameters: "timeline[]" => timelines.map(\.rawValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func update<T: TimelineMarkerType>(timeline: T, lastReadID: String) -> Request<Empty> {
|
public static func update(timeline: Timeline, lastReadID: String) -> Request<Empty> {
|
||||||
Request(method: .post, path: "/api/v1/markers", body: ParametersBody([
|
return Request(method: .post, path: "/api/v1/markers", body: ParametersBody([
|
||||||
"\(T.name)[last_read_id]" => lastReadID
|
"\(timeline.rawValue)[last_read_id]" => lastReadID,
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Timeline: String {
|
||||||
|
case home
|
||||||
|
case notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct TimelineMarker<Payload: TimelineMarkerTypePayload>: Decodable, Sendable {
|
public struct Marker: Decodable, Sendable {
|
||||||
let payload: Payload
|
|
||||||
|
|
||||||
public var lastReadID: String {
|
|
||||||
payload.payload.lastReadID
|
|
||||||
}
|
|
||||||
public var version: Int {
|
|
||||||
payload.payload.version
|
|
||||||
}
|
|
||||||
public var updatedAt: Date {
|
|
||||||
payload.payload.updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(from decoder: any Decoder) throws {
|
|
||||||
self.payload = try Payload(from: decoder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public protocol TimelineMarkerTypePayload: Decodable, Sendable {
|
|
||||||
var payload: MarkerPayload { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct HomeMarkerPayload: TimelineMarkerTypePayload {
|
|
||||||
public var home: MarkerPayload
|
|
||||||
public var payload: MarkerPayload { home }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct NotificationsMarkerPayload: TimelineMarkerTypePayload {
|
|
||||||
public var notifications: MarkerPayload
|
|
||||||
public var payload: MarkerPayload { notifications }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct MarkerPayload: Decodable, Sendable {
|
|
||||||
public let lastReadID: String
|
public let lastReadID: String
|
||||||
public let version: Int
|
public let version: Int
|
||||||
public let updatedAt: Date
|
public let updatedAt: Date
|
||||||
|
@ -64,26 +37,4 @@ public struct MarkerPayload: Decodable, Sendable {
|
||||||
case updatedAt = "updated_at"
|
case updatedAt = "updated_at"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol TimelineMarkerType {
|
|
||||||
static var name: String { get }
|
|
||||||
associatedtype Payload: TimelineMarkerTypePayload
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineMarkerType where Self == HomeMarker {
|
|
||||||
public static var home: Self { .init() }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineMarkerType where Self == NotificationsMarker {
|
|
||||||
public static var notifications: Self { .init() }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct HomeMarker: TimelineMarkerType {
|
|
||||||
public typealias Payload = HomeMarkerPayload
|
|
||||||
public static var name: String { "home" }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct NotificationsMarker: TimelineMarkerType {
|
|
||||||
public typealias Payload = NotificationsMarkerPayload
|
|
||||||
public static var name: String { "notifications" }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -87,7 +84,6 @@ public final class Preferences: Codable, ObservableObject {
|
||||||
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
||||||
self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true
|
self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true
|
||||||
self.showAttachmentBadges = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentBadges) ?? true
|
self.showAttachmentBadges = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentBadges) ?? true
|
||||||
self.attachmentAltBadgeInverted = try container.decodeIfPresent(Bool.self, forKey: .attachmentAltBadgeInverted) ?? false
|
|
||||||
|
|
||||||
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
||||||
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||||
|
@ -146,7 +142,6 @@ public final class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
|
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
|
||||||
try container.encode(showUncroppedMediaInline, forKey: .showUncroppedMediaInline)
|
try container.encode(showUncroppedMediaInline, forKey: .showUncroppedMediaInline)
|
||||||
try container.encode(showAttachmentBadges, forKey: .showAttachmentBadges)
|
try container.encode(showAttachmentBadges, forKey: .showAttachmentBadges)
|
||||||
try container.encode(attachmentAltBadgeInverted, forKey: .attachmentAltBadgeInverted)
|
|
||||||
|
|
||||||
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
||||||
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
||||||
|
@ -213,7 +208,6 @@ public final class Preferences: Codable, ObservableObject {
|
||||||
@Published public var automaticallyPlayGifs = true
|
@Published public var automaticallyPlayGifs = true
|
||||||
@Published public var showUncroppedMediaInline = true
|
@Published public var showUncroppedMediaInline = true
|
||||||
@Published public var showAttachmentBadges = true
|
@Published public var showAttachmentBadges = true
|
||||||
@Published public var attachmentAltBadgeInverted = false
|
|
||||||
|
|
||||||
// MARK: Behavior
|
// MARK: Behavior
|
||||||
@Published public var openLinksInApps = true
|
@Published public var openLinksInApps = true
|
||||||
|
@ -277,7 +271,6 @@ public final class Preferences: Codable, ObservableObject {
|
||||||
case automaticallyPlayGifs
|
case automaticallyPlayGifs
|
||||||
case showUncroppedMediaInline
|
case showUncroppedMediaInline
|
||||||
case showAttachmentBadges
|
case showAttachmentBadges
|
||||||
case attachmentAltBadgeInverted
|
|
||||||
|
|
||||||
case openLinksInApps
|
case openLinksInApps
|
||||||
case useInAppSafari
|
case useInAppSafari
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 */; };
|
||||||
|
@ -30,7 +32,7 @@
|
||||||
D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; };
|
D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; };
|
||||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
|
||||||
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
||||||
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D60BB3932B30076F00DAEA65 /* HTMLStreamer */; };
|
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
||||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
|
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
|
||||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
|
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
|
||||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; };
|
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.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 */; };
|
||||||
|
@ -255,18 +252,17 @@
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
|
||||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
||||||
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */; };
|
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */; };
|
||||||
|
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B9366A281EE77E00237D0E /* PollVoteButton.swift */; };
|
||||||
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 */; };
|
||||||
|
@ -281,7 +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 */; };
|
||||||
|
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 */; };
|
||||||
|
@ -304,17 +303,18 @@
|
||||||
D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; };
|
D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; };
|
||||||
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */; };
|
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */; };
|
||||||
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; };
|
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; };
|
||||||
|
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */; };
|
||||||
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; };
|
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; };
|
||||||
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 */; };
|
||||||
|
@ -322,7 +322,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 */; };
|
||||||
|
@ -408,11 +408,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>"; };
|
||||||
|
@ -483,8 +485,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>"; };
|
||||||
|
@ -508,8 +512,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>"; };
|
||||||
|
@ -518,13 +523,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>"; };
|
||||||
|
@ -547,11 +554,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>"; };
|
||||||
|
@ -559,6 +568,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>"; };
|
||||||
|
@ -574,7 +584,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>"; };
|
||||||
|
@ -582,26 +592,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>"; };
|
||||||
|
@ -637,7 +634,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>"; };
|
||||||
|
@ -646,7 +643,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>"; };
|
||||||
|
@ -656,19 +653,18 @@
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
||||||
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarMyProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarMyProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D6B9366A281EE77E00237D0E /* PollVoteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollVoteButton.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
@ -683,8 +679,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>"; };
|
||||||
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TuskerPreferences; sourceTree = "<group>"; };
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; 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>"; };
|
||||||
|
@ -713,17 +712,18 @@
|
||||||
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = "<group>"; };
|
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = "<group>"; };
|
||||||
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
|
||||||
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; };
|
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
@ -759,7 +759,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 */
|
||||||
|
|
||||||
|
@ -786,11 +786,10 @@
|
||||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
||||||
D659F35E2953A212002D944A /* TTTKit in Frameworks */,
|
D659F35E2953A212002D944A /* TTTKit in Frameworks */,
|
||||||
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
|
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
|
||||||
|
D60CFFDB24A290BA00D00083 /* SwiftSoup 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 */,
|
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
||||||
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
||||||
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
|
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
|
||||||
|
@ -822,20 +821,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 */ = {
|
||||||
|
@ -894,6 +888,7 @@
|
||||||
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */,
|
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */,
|
||||||
D623A5402635FB3C0095BD04 /* PollOptionView.swift */,
|
D623A5402635FB3C0095BD04 /* PollOptionView.swift */,
|
||||||
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */,
|
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */,
|
||||||
|
D6B9366A281EE77E00237D0E /* PollVoteButton.swift */,
|
||||||
);
|
);
|
||||||
path = Poll;
|
path = Poll;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -911,10 +906,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 */,
|
||||||
|
@ -994,13 +988,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 */,
|
||||||
|
@ -1103,6 +1098,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 = (
|
||||||
|
@ -1154,21 +1162,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 */ = {
|
||||||
|
@ -1225,6 +1226,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 */,
|
||||||
|
@ -1236,6 +1238,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 */,
|
||||||
|
@ -1313,7 +1316,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>";
|
||||||
|
@ -1350,8 +1352,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 */,
|
||||||
|
@ -1396,6 +1396,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 */,
|
||||||
|
@ -1442,8 +1443,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;
|
||||||
|
@ -1455,7 +1455,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 */,
|
||||||
|
@ -1485,11 +1493,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 */,
|
||||||
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 */,
|
||||||
|
@ -1497,7 +1504,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 */,
|
||||||
|
@ -1568,6 +1574,7 @@
|
||||||
children = (
|
children = (
|
||||||
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */,
|
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */,
|
||||||
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */,
|
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */,
|
||||||
|
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */,
|
||||||
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */,
|
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */,
|
||||||
);
|
);
|
||||||
path = "Status Edit History";
|
path = "Status Edit History";
|
||||||
|
@ -1692,6 +1699,7 @@
|
||||||
);
|
);
|
||||||
name = Tusker;
|
name = Tusker;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
|
||||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
||||||
D674A50827F9128D00BA03AC /* Pachyderm */,
|
D674A50827F9128D00BA03AC /* Pachyderm */,
|
||||||
D6552366289870790048A653 /* ScreenCorners */,
|
D6552366289870790048A653 /* ScreenCorners */,
|
||||||
|
@ -1703,8 +1711,6 @@
|
||||||
D635237029B78A7D009ED5E7 /* TuskerComponents */,
|
D635237029B78A7D009ED5E7 /* TuskerComponents */,
|
||||||
D6BD395829B64426005FFD2B /* ComposeUI */,
|
D6BD395829B64426005FFD2B /* ComposeUI */,
|
||||||
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
|
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
|
||||||
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
|
|
||||||
D6934F2B2BA7AD32002B1C8D /* GalleryVC */,
|
|
||||||
);
|
);
|
||||||
productName = Tusker;
|
productName = Tusker;
|
||||||
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
||||||
|
@ -1815,10 +1821,10 @@
|
||||||
);
|
);
|
||||||
mainGroup = D6D4DDC3212518A000E1C4BB;
|
mainGroup = D6D4DDC3212518A000E1C4BB;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
|
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||||
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
||||||
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
||||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||||
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
|
|
||||||
);
|
);
|
||||||
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
|
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -1838,7 +1844,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;
|
||||||
|
@ -1850,12 +1855,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 */,
|
||||||
|
@ -1949,7 +1955,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 */,
|
||||||
|
@ -1978,9 +1984,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 */,
|
||||||
|
@ -1989,17 +1993,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 */,
|
||||||
|
@ -2008,14 +2010,15 @@
|
||||||
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 */,
|
||||||
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
||||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
||||||
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
||||||
|
@ -2030,23 +2033,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 */,
|
||||||
|
@ -2068,6 +2071,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 */,
|
||||||
|
@ -2076,7 +2080,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 */,
|
||||||
|
@ -2111,6 +2114,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 */,
|
||||||
|
@ -2138,6 +2142,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 */,
|
||||||
|
@ -2146,7 +2151,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 */,
|
||||||
|
@ -2156,8 +2160,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 */,
|
||||||
|
@ -2166,10 +2168,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 */,
|
||||||
|
@ -2179,12 +2181,15 @@
|
||||||
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 */,
|
||||||
|
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.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 */,
|
||||||
|
@ -2194,11 +2199,14 @@
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
|
||||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
||||||
|
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */,
|
||||||
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 */,
|
||||||
|
@ -2211,11 +2219,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 */,
|
||||||
|
@ -2227,7 +2235,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 */,
|
||||||
|
@ -2292,6 +2299,7 @@
|
||||||
};
|
};
|
||||||
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = {
|
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
|
platformFilter = ios;
|
||||||
target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */;
|
target = D6E343A7265AAD6B00C4AA01 /* OpenInTusker */;
|
||||||
targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */;
|
targetProxy = D6E343B2265AAD6B00C4AA01 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
@ -2413,11 +2421,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;
|
||||||
};
|
};
|
||||||
|
@ -2448,7 +2455,6 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2481,10 +2487,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;
|
||||||
};
|
};
|
||||||
|
@ -2509,11 +2514,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;
|
||||||
};
|
};
|
||||||
|
@ -2538,11 +2542,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;
|
||||||
};
|
};
|
||||||
|
@ -2567,11 +2570,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;
|
||||||
};
|
};
|
||||||
|
@ -2721,13 +2723,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;
|
||||||
};
|
};
|
||||||
|
@ -2752,11 +2753,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;
|
||||||
};
|
};
|
||||||
|
@ -2808,7 +2808,6 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2828,7 +2827,6 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2861,10 +2859,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;
|
||||||
};
|
};
|
||||||
|
@ -2887,10 +2884,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;
|
||||||
};
|
};
|
||||||
|
@ -2960,12 +2956,12 @@
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */ = {
|
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git";
|
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = upToNextMinorVersion;
|
||||||
version = 0.2.3;
|
minimumVersion = 2.3.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
||||||
|
@ -2973,7 +2969,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" */ = {
|
||||||
|
@ -2995,10 +2991,10 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
D60BB3932B30076F00DAEA65 /* HTMLStreamer */ = {
|
D60CFFDA24A290BA00D00083 /* SwiftSoup */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
|
package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||||
productName = HTMLStreamer;
|
productName = SwiftSoup;
|
||||||
};
|
};
|
||||||
D61ABEFB28F105DE00B29151 /* Pachyderm */ = {
|
D61ABEFB28F105DE00B29151 /* Pachyderm */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
@ -3031,10 +3027,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;
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import HTMLStreamer
|
import SwiftSoup
|
||||||
|
|
||||||
class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
let status: StatusMO
|
let status: StatusMO
|
||||||
|
@ -33,8 +33,8 @@ 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)
|
let doc = try! SwiftSoup.parse(status.content)
|
||||||
let content = converter.convert(html: status.content)
|
let content = try! doc.text()
|
||||||
metadata.title = "\(status.account.displayName): \"\(content)\""
|
metadata.title = "\(status.account.displayName): \"\(content)\""
|
||||||
if let avatar = status.account.avatar,
|
if let avatar = status.account.avatar,
|
||||||
let data = ImageCache.avatars.getData(avatar),
|
let data = ImageCache.avatars.getData(avatar),
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,20 +19,21 @@ 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)
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
AudioSessionHelper.disable()
|
||||||
|
AudioSessionHelper.setDefault()
|
||||||
|
}
|
||||||
|
|
||||||
if let oldSavedData = SavedDataManager.load() {
|
if let oldSavedData = SavedDataManager.load() {
|
||||||
do {
|
do {
|
||||||
for account in oldSavedData.accountIDs {
|
for account in oldSavedData.accountIDs {
|
||||||
|
@ -57,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,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 {
|
||||||
|
@ -118,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)
|
||||||
}
|
}
|
||||||
|
@ -168,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?
|
||||||
|
@ -220,6 +213,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
Logging.general.error("Unable to swizzle presentation controller")
|
Logging.general.error("Unable to swizzle presentation controller")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// AudioSessionHelper.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 6/21/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
struct AudioSessionHelper {
|
||||||
|
static func enable() {
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(true, options: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func disable() {
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func setDefault() {
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func setVideoPlayback() {
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [])
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)...]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,42 +8,94 @@
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
func trimLeadingCharactersInSet(_ charSet: CharacterSet) {
|
func trimLeadingCharactersInSet(_ charSet: CharacterSet) {
|
||||||
var end = string.startIndex
|
var range = (string as NSString).rangeOfCharacter(from: charSet)
|
||||||
while end < string.endIndex && charSet.contains(string.unicodeScalars[end]) {
|
|
||||||
end = string.unicodeScalars.index(after: end)
|
while range.length != 0 && range.location == 0 {
|
||||||
}
|
replaceCharacters(in: range, with: "")
|
||||||
if end > string.startIndex {
|
range = (string as NSString).rangeOfCharacter(from: charSet)
|
||||||
let length = string.utf16.distance(from: string.startIndex, to: end)
|
|
||||||
replaceCharacters(in: NSRange(location: 0, length: length), with: "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimTrailingCharactersInSet(_ charSet: CharacterSet) {
|
func trimTrailingCharactersInSet(_ charSet: CharacterSet) {
|
||||||
if string.isEmpty {
|
var range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
|
||||||
return
|
|
||||||
|
while range.length != 0 && range.length + range.location == length {
|
||||||
|
replaceCharacters(in: range, with: "")
|
||||||
|
range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
|
||||||
}
|
}
|
||||||
var start = string.index(before: string.endIndex)
|
|
||||||
while start > string.startIndex && charSet.contains(string.unicodeScalars[start]) {
|
|
||||||
start = string.unicodeScalars.index(before: start)
|
|
||||||
}
|
}
|
||||||
if start < string.endIndex {
|
|
||||||
if start != string.startIndex || !charSet.contains(string.unicodeScalars[start]) {
|
/// Collapses whitespace in this string according to the CSS Text Module Level 3 rules.
|
||||||
start = string.unicodeScalars.index(after: start)
|
/// 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
|
||||||
}
|
}
|
||||||
let location = string.utf16.distance(from: string.startIndex, to: start)
|
j -= 1
|
||||||
let length = string.utf16.distance(from: start, to: string.endIndex)
|
}
|
||||||
replaceCharacters(in: NSRange(location: location, length: length), with: "")
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, *) {
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Filterer {
|
||||||
|
|
||||||
var filtersChanged: ((Bool) -> Void)?
|
var filtersChanged: ((Bool) -> Void)?
|
||||||
|
|
||||||
private var htmlConverter: HTMLConverter
|
var htmlConverter = HTMLConverter()
|
||||||
private var hasSetup = false
|
private var hasSetup = false
|
||||||
private var matchers = [(NSRegularExpression, Result)]()
|
private var matchers = [(NSRegularExpression, Result)]()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
@ -55,10 +55,9 @@ class Filterer {
|
||||||
// are no longer valid, without needing to go through and update each of them
|
// are no longer valid, without needing to go through and update each of them
|
||||||
private var generation = 0
|
private var generation = 0
|
||||||
|
|
||||||
init(mastodonController: MastodonController, context: FilterV1.Context, htmlConverter: HTMLConverter) {
|
init(mastodonController: MastodonController, context: FilterV1.Context) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.context = context
|
self.context = context
|
||||||
self.htmlConverter = htmlConverter
|
|
||||||
self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines
|
self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines
|
||||||
self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines
|
self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import HTMLStreamer
|
import SwiftSoup
|
||||||
import WebURL
|
import WebURL
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
class HTMLConverter {
|
struct HTMLConverter {
|
||||||
|
|
||||||
static let defaultFont = UIFont.systemFont(ofSize: 17)
|
static let defaultFont = UIFont.systemFont(ofSize: 17)
|
||||||
static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular)
|
static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular)
|
||||||
|
@ -23,47 +23,150 @@ class HTMLConverter {
|
||||||
return style
|
return style
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let converter: AttributedStringConverter<Callbacks>
|
var font: UIFont = defaultFont
|
||||||
|
var monospaceFont: UIFont = defaultMonospaceFont
|
||||||
init(font: UIFont, monospaceFont: UIFont, color: UIColor, paragraphStyle: NSParagraphStyle) {
|
var color: UIColor = defaultColor
|
||||||
let config = AttributedStringConverterConfiguration(font: font, monospaceFont: monospaceFont, color: color, paragraphStyle: paragraphStyle)
|
var paragraphStyle: NSParagraphStyle = defaultParagraphStyle
|
||||||
self.converter = AttributedStringConverter(configuration: config)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convert(_ html: String) -> NSAttributedString {
|
func convert(_ html: String) -> NSAttributedString {
|
||||||
converter.convert(html: html)
|
let doc = try! SwiftSoup.parseBodyFragment(html)
|
||||||
|
let body = doc.body()!
|
||||||
|
|
||||||
|
if let attributedText = attributedTextForHTMLNode(body) {
|
||||||
|
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
|
||||||
|
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
mutAttrString.collapseWhitespace()
|
||||||
|
|
||||||
|
// Wait until the end and then fill in the unset paragraph styles, to avoid clobbering the list style.
|
||||||
|
mutAttrString.enumerateAttribute(.paragraphStyle, in: mutAttrString.fullRange, options: .longestEffectiveRangeNotRequired) { value, range, stop in
|
||||||
|
if value == nil {
|
||||||
|
mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HTMLConverter {
|
return mutAttrString
|
||||||
struct Callbacks: HTMLConversionCallbacks {
|
|
||||||
static func makeURL(string: String) -> URL? {
|
|
||||||
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
|
||||||
// serializing the WebURL as a string and then having Foundation parse it again),
|
|
||||||
// so, if available, use the system parser which doesn't require another round trip.
|
|
||||||
if #available(iOS 16.0, macOS 13.0, *),
|
|
||||||
let url = try? URL.ParseStrategy().parse(string) {
|
|
||||||
url
|
|
||||||
} else if let web = WebURL(string),
|
|
||||||
let url = URL(web) {
|
|
||||||
url
|
|
||||||
} else {
|
} else {
|
||||||
URL(string: string)
|
return NSAttributedString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
private func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString? {
|
||||||
guard name == "span" else {
|
switch node {
|
||||||
return .default
|
case let node as TextNode:
|
||||||
}
|
let text: String
|
||||||
let clazz = attributes.attributeValue(for: "class")
|
if usePreformattedText {
|
||||||
if clazz == "invisible" {
|
text = node.getWholeText()
|
||||||
return .skip
|
|
||||||
} else if clazz == "ellipsis" {
|
|
||||||
return .append("…")
|
|
||||||
} else {
|
} else {
|
||||||
return .default
|
text = node.text()
|
||||||
|
}
|
||||||
|
return NSAttributedString(string: text, attributes: [.font: font, .foregroundColor: color])
|
||||||
|
case let node as Element:
|
||||||
|
if node.tagName() == "ol" || node.tagName() == "ul" {
|
||||||
|
return attributedTextForList(node, usePreformattedText: usePreformattedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
||||||
|
for child in node.getChildNodes() {
|
||||||
|
var appendEllipsis = false
|
||||||
|
if node.tagName() == "a",
|
||||||
|
let el = child as? Element {
|
||||||
|
if el.hasClass("invisible") {
|
||||||
|
continue
|
||||||
|
} else if el.hasClass("ellipsis") {
|
||||||
|
appendEllipsis = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let childText = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre") {
|
||||||
|
attributed.append(childText)
|
||||||
|
}
|
||||||
|
|
||||||
|
if appendEllipsis {
|
||||||
|
attributed.append(NSAttributedString("…"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var currentFont = if attributed.length == 0 {
|
||||||
|
font
|
||||||
|
} else {
|
||||||
|
attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? font
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node.tagName() {
|
||||||
|
case "br":
|
||||||
|
// need to specify defaultFont here b/c otherwise it uses the default 12pt Helvetica which
|
||||||
|
// screws up its determination of the line height making multiple lines of emojis squash together
|
||||||
|
attributed.append(NSAttributedString(string: "\n", attributes: [.font: font]))
|
||||||
|
case "a":
|
||||||
|
let href = try! node.attr("href")
|
||||||
|
if let webURL = WebURL(href),
|
||||||
|
let url = URL(webURL) {
|
||||||
|
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
||||||
|
} else if let url = URL(string: href) {
|
||||||
|
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
||||||
|
}
|
||||||
|
case "p":
|
||||||
|
attributed.append(NSAttributedString(string: "\n\n", attributes: [.font: font]))
|
||||||
|
case "em", "i":
|
||||||
|
attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange)
|
||||||
|
case "strong", "b":
|
||||||
|
attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange)
|
||||||
|
case "del":
|
||||||
|
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
||||||
|
case "code":
|
||||||
|
attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange)
|
||||||
|
case "pre":
|
||||||
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
|
attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange)
|
||||||
|
case "blockquote":
|
||||||
|
let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
|
||||||
|
paragraphStyle.headIndent = 32
|
||||||
|
paragraphStyle.firstLineHeadIndent = 32
|
||||||
|
attributed.addAttributes([
|
||||||
|
.font: currentFont.withTraits(.traitItalic)!,
|
||||||
|
.paragraphStyle: paragraphStyle,
|
||||||
|
], range: attributed.fullRange)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributed
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func attributedTextForList(_ element: Element, usePreformattedText: Bool) -> NSAttributedString {
|
||||||
|
let list = element.tagName() == "ol" ? OrderedNumberTextList(markerFormat: .decimal, options: 0) : NSTextList(markerFormat: .disc, options: 0)
|
||||||
|
let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
|
||||||
|
// I don't like that I can't just use paragraphStyle.textLists, because it makes the list markers
|
||||||
|
// not use the monospace digit font (it seems to just use whatever font attribute is set for the whole thing),
|
||||||
|
// and it doesn't right align the list markers.
|
||||||
|
// Unfortunately, doing it manually means the list markers are incldued in the selectable text.
|
||||||
|
paragraphStyle.headIndent = 32
|
||||||
|
paragraphStyle.firstLineHeadIndent = 0
|
||||||
|
// Use 2 tab stops, one for the list marker, the second for the content.
|
||||||
|
paragraphStyle.tabStops = [NSTextTab(textAlignment: .right, location: 28), NSTextTab(textAlignment: .natural, location: 32)]
|
||||||
|
let str = NSMutableAttributedString(string: "")
|
||||||
|
var item = 1
|
||||||
|
for child in element.children() where child.tagName() == "li" {
|
||||||
|
if let childStr = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText) {
|
||||||
|
str.append(NSAttributedString(string: "\t\(list.marker(forItemNumber: item))\t", attributes: [
|
||||||
|
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .monospacedDigitSystemFont(ofSize: 17, weight: .regular)),
|
||||||
|
]))
|
||||||
|
str.append(childStr)
|
||||||
|
str.append(NSAttributedString(string: "\n"))
|
||||||
|
item += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str.addAttribute(.paragraphStyle, value: paragraphStyle, range: str.fullRange)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OrderedNumberTextList: NSTextList {
|
||||||
|
override func marker(forItemNumber itemNumber: Int) -> String {
|
||||||
|
"\(super.marker(forItemNumber: itemNumber))."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@MainActor
|
|
||||||
struct MenuController {
|
struct MenuController {
|
||||||
|
|
||||||
static let composeCommand: UIKeyCommand = {
|
static let composeCommand: UIKeyCommand = {
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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>
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -52,12 +52,6 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
|
||||||
config.topSeparatorVisibility = .hidden
|
config.topSeparatorVisibility = .hidden
|
||||||
config.bottomSeparatorVisibility = .hidden
|
config.bottomSeparatorVisibility = .hidden
|
||||||
}
|
}
|
||||||
if config.topSeparatorInsets != .zero {
|
|
||||||
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
|
||||||
}
|
|
||||||
if config.bottomSeparatorInsets != .zero {
|
|
||||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
|
||||||
}
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,6 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
var config = UICollectionLayoutListConfiguration(appearance: .grouped)
|
||||||
config.backgroundColor = .appGroupedBackground
|
config.backgroundColor = .appGroupedBackground
|
||||||
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
|
||||||
config.separatorConfiguration.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
section.readableContentInset(in: environment)
|
section.readableContentInset(in: environment)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -26,10 +26,11 @@ struct ComposeReplyContentView: UIViewRepresentable {
|
||||||
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
|
||||||
view.adjustsFontForContentSizeCategory = true
|
view.adjustsFontForContentSizeCategory = true
|
||||||
|
view.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||||
|
view.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||||
|
|
||||||
view.overrideMastodonController = mastodonController
|
view.overrideMastodonController = mastodonController
|
||||||
let content = TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
|
view.setTextFrom(status: status)
|
||||||
view.attributedText = content
|
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>!
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -37,6 +34,8 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
||||||
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
||||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
|
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
|
||||||
|
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||||
noteTextView.adjustsFontForContentSizeCategory = true
|
noteTextView.adjustsFontForContentSizeCategory = true
|
||||||
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
|
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
|
||||||
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
|
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
|
||||||
|
@ -61,39 +60,41 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
|
|
||||||
noteTextView.setBodyTextFromHTML(account.note)
|
noteTextView.setTextFromHtml(account.note)
|
||||||
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 +108,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()
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue