Compare commits
63 Commits
ef1db466b9
...
e61823b78f
Author | SHA1 | Date |
---|---|---|
Shadowfacts | e61823b78f | |
Shadowfacts | 4d52ac4d34 | |
Shadowfacts | aced0a63c9 | |
Shadowfacts | 1e54235ff5 | |
Shadowfacts | e6e5554edf | |
Shadowfacts | 9026f487ec | |
Shadowfacts | c0097ba752 | |
Shadowfacts | f109253bba | |
Shadowfacts | 1fda4248ec | |
Shadowfacts | 7781c5252b | |
Shadowfacts | 7f4bf52050 | |
Shadowfacts | ba0d179de5 | |
Shadowfacts | 71b6f1bdf0 | |
Shadowfacts | 09ec4a920c | |
Shadowfacts | 7edf0fdb93 | |
Shadowfacts | 99e06441f0 | |
Shadowfacts | 85e1e131f6 | |
Shadowfacts | 1d79918a94 | |
Shadowfacts | 340d13b1fa | |
Shadowfacts | cf1000a4df | |
Shadowfacts | b781b56efd | |
Shadowfacts | 10a8a85bfc | |
Shadowfacts | 6d8a014cc7 | |
Shadowfacts | 60c88ded5e | |
Shadowfacts | 1e7a6af0bf | |
Shadowfacts | f8b79ef34f | |
Shadowfacts | 4cf56685b5 | |
Shadowfacts | fdcd2aa540 | |
Shadowfacts | 667d30a710 | |
Shadowfacts | b0f23e46ba | |
Shadowfacts | 9b30b48016 | |
Shadowfacts | bd49683e13 | |
Shadowfacts | c22945b1e7 | |
Shadowfacts | 0a16a2e261 | |
Shadowfacts | b95819cada | |
Shadowfacts | dc1ea1bed9 | |
Shadowfacts | 5f9fe505d5 | |
Shadowfacts | 5b8e97287e | |
Shadowfacts | 49572c1fec | |
Shadowfacts | ebb0770198 | |
Shadowfacts | 27e05cc72d | |
Shadowfacts | 4ca48a5f50 | |
Shadowfacts | 230bd50661 | |
Shadowfacts | 4f2f8d517f | |
Shadowfacts | 130da9d4cc | |
Shadowfacts | 472b9aa5e2 | |
Shadowfacts | 3413dff8f9 | |
Shadowfacts | 66e8fce488 | |
Shadowfacts | aa2d333f4a | |
Shadowfacts | c8a45d8eef | |
Shadowfacts | 40f5be28f6 | |
Shadowfacts | 7c9287543c | |
Shadowfacts | 2a05b6d326 | |
Shadowfacts | 2499d25432 | |
Shadowfacts | 9417872790 | |
Shadowfacts | c02a1bbf74 | |
Shadowfacts | 0a894b219a | |
Shadowfacts | 22803668d2 | |
Shadowfacts | 2f6d1cb069 | |
Shadowfacts | 8889261b6b | |
Shadowfacts | 91f1a5195c | |
Shadowfacts | 1a5b958b1a | |
Shadowfacts | d667f6362c |
60
CHANGELOG.md
60
CHANGELOG.md
|
@ -1,5 +1,65 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2021.1 (22)
|
||||||
|
This is the first public beta build of Tusker, so if you're just joining us, welcome! Not too many new features this build, mostly bugfixes, so test everything and generally use the app.
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- Add timeline descriptions the first time you view federated/local
|
||||||
|
- Show messages when loading posts fails or when there are no newer posts
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash after editing lists
|
||||||
|
- Fix crash when refreshing before anything is loaded
|
||||||
|
- Fix crash when fetching recommended instances fails
|
||||||
|
- Fix crash when replying to posts with code formatting
|
||||||
|
- Fix crash when changing preferences after switching accounts
|
||||||
|
|
||||||
|
## 2021.1 (21)
|
||||||
|
This is a quick follow-up to the previous build with fixes for a couple major crashes. Unfortunately, due to a bug in iOS 14, the Disable Infinite Scrolling preference now requires the iOS 15 beta to use. It may return in a future build if I can find a workaround, but it's disabled in the meantime.
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- iPadOS 15: Add Open in New Window context menu action to sidebar items
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash when editing accounts in a list
|
||||||
|
- Fix crash when refreshing timeline on iOS 14
|
||||||
|
- Fix(ish) crash when opening collapsed status with Disable Infinite Scrolling active on iOS 15
|
||||||
|
|
||||||
|
## 2021.1 (20)
|
||||||
|
This is a big one! In addition to a bunch of fixes for anyone on the iOS 15 beta, there are a couple of big ticket features, including the Open in Tusker action extension and the Disable Infinite Scrolling preference.
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- Add Open in Tusker action extension
|
||||||
|
- Quickly search for any URL in Tusker
|
||||||
|
- In a share sheet, scroll to the bottom, tap "Edit Actions..." and turn on the "Open in Tusker" action
|
||||||
|
- Add Digital Wellness preference to disable infinite scrolling
|
||||||
|
- Add fast account switching indicator to My Profile tab
|
||||||
|
- Improve VoiceOver accessibility of polls and timeline statuses
|
||||||
|
- iPadOS: Create multiple main windows for different accounts by dragging from an account in Preferences
|
||||||
|
- iPadOS: Delete attachments on Compose screen by right-clicking and selecting Delete
|
||||||
|
- iPadOS 15: Add Open in New Window context menu action to most things
|
||||||
|
- iPadOS 15: Allow dragging the Compose sheet into a separate window
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix being unable to commit previewed account from timeline status
|
||||||
|
- Fix crash when searching fails
|
||||||
|
- Fix poll option percentages being cut off
|
||||||
|
- Fix polls not collapsing inside CWs
|
||||||
|
- Fix More button on profiles not being accessible with VoiceOver
|
||||||
|
- Fix VoiceOver reading profile fields in incorrect order
|
||||||
|
- Fix gallery animations jittering on devices with square screens (iPads, non-notched iPhones)
|
||||||
|
- Fix CW text jumping around post collapse animation
|
||||||
|
- iOS 15: Fix crash due when showing Draw Something screen in Compose
|
||||||
|
- iPadOS 14/iOS 15: Fix navigation bar turning transparent after opening the attachment gallery
|
||||||
|
- iPadOS 14/iOS 15: Fix drag-selecting poll options initiating a status cell drag interaction
|
||||||
|
- iPadOS: Fix crash when loading a previously-opened conversation window
|
||||||
|
- iPadOS 15: Fix showing Compose screen when keyboard focus moves through the sidebar
|
||||||
|
|
||||||
|
Known Issues:
|
||||||
|
- Disable Infinite Scrolling preference only affects timelines, not notifications or profiles
|
||||||
|
- iPadOS 15: The Compose sheet cannot be dismissed by swiping down
|
||||||
|
- iPadOS 15: Keyboard focus is stuck in the sidebar
|
||||||
|
|
||||||
## 2021.1 (19)
|
## 2021.1 (19)
|
||||||
This is an emergency fix for Tusker breaking when connecting to Mastodon instances on 3.4.0rc1.
|
This is an emergency fix for Tusker breaking when connecting to Mastodon instances on 3.4.0rc1.
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionAttributes</key>
|
<key>NSExtensionAttributes</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionJavaScriptPreprocessingFile</key>
|
|
||||||
<string>Action</string>
|
|
||||||
<key>NSExtensionActivationRule</key>
|
<key>NSExtensionActivationRule</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
|
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
|
||||||
|
@ -35,6 +33,8 @@
|
||||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSExtensionJavaScriptPreprocessingFile</key>
|
||||||
|
<string>Action</string>
|
||||||
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
|
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSExtensionServiceAllowsTouchBarItem</key>
|
<key>NSExtensionServiceAllowsTouchBarItem</key>
|
||||||
|
|
|
@ -18,12 +18,21 @@ public class Instance: Decodable {
|
||||||
public let thumbnail: URL?
|
public let thumbnail: URL?
|
||||||
public let languages: [String]?
|
public let languages: [String]?
|
||||||
public let stats: Stats?
|
public let stats: Stats?
|
||||||
|
public let configuration: Configuration?
|
||||||
|
|
||||||
// pleroma doesn't currently implement these
|
// pleroma doesn't currently implement these
|
||||||
public let contactAccount: Account?
|
public let contactAccount: Account?
|
||||||
|
|
||||||
// MARK: Unofficial additions to the Mastodon API.
|
// superseded by mastodon's configuration.statuses.max_characters, still used by older instances & pleroma
|
||||||
public let maxStatusCharacters: Int?
|
let maxTootCharacters: Int?
|
||||||
|
let pollLimits: PollsConfiguration?
|
||||||
|
|
||||||
|
public var maxStatusCharacters: Int? {
|
||||||
|
configuration?.statuses.maxCharacters ?? maxTootCharacters
|
||||||
|
}
|
||||||
|
public var pollsConfiguration: PollsConfiguration? {
|
||||||
|
configuration?.polls ?? pollLimits
|
||||||
|
}
|
||||||
|
|
||||||
// we need a custom decoder, because all API-compatible implementations don't return some data in the same format
|
// we need a custom decoder, because all API-compatible implementations don't return some data in the same format
|
||||||
public required init(from decoder: Decoder) throws {
|
public required init(from decoder: Decoder) throws {
|
||||||
|
@ -44,14 +53,18 @@ public class Instance: Decodable {
|
||||||
|
|
||||||
self.stats = try? container.decodeIfPresent(Stats.self, forKey: .stats)
|
self.stats = try? container.decodeIfPresent(Stats.self, forKey: .stats)
|
||||||
self.thumbnail = try? container.decodeIfPresent(URL.self, forKey: .thumbnail)
|
self.thumbnail = try? container.decodeIfPresent(URL.self, forKey: .thumbnail)
|
||||||
if let maxStatusCharacters = try? container.decodeIfPresent(Int.self, forKey: .maxStatusCharacters) {
|
|
||||||
self.maxStatusCharacters = maxStatusCharacters
|
self.configuration = try? container.decodeIfPresent(Configuration.self, forKey: .configuration)
|
||||||
} else if let str = try? container.decodeIfPresent(String.self, forKey: .maxStatusCharacters),
|
|
||||||
let maxStatusCharacters = Int(str, radix: 10) {
|
if let maxTootCharacters = try? container.decodeIfPresent(Int.self, forKey: .maxTootCharacters) {
|
||||||
self.maxStatusCharacters = maxStatusCharacters
|
self.maxTootCharacters = maxTootCharacters
|
||||||
|
} else if let str = try? container.decodeIfPresent(String.self, forKey: .maxTootCharacters),
|
||||||
|
let maxTootCharacters = Int(str, radix: 10) {
|
||||||
|
self.maxTootCharacters = maxTootCharacters
|
||||||
} else {
|
} else {
|
||||||
self.maxStatusCharacters = nil
|
self.maxTootCharacters = nil
|
||||||
}
|
}
|
||||||
|
self.pollLimits = try? container.decodeIfPresent(PollsConfiguration.self, forKey: .pollLimits)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,15 +78,16 @@ public class Instance: Decodable {
|
||||||
case thumbnail
|
case thumbnail
|
||||||
case languages
|
case languages
|
||||||
case stats
|
case stats
|
||||||
|
case configuration
|
||||||
case contactAccount = "contact_account"
|
case contactAccount = "contact_account"
|
||||||
|
|
||||||
case maxStatusCharacters = "max_toot_chars"
|
case maxTootCharacters = "max_toot_chars"
|
||||||
|
case pollLimits = "poll_limits"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Instance {
|
extension Instance {
|
||||||
public class Stats: Decodable {
|
public struct Stats: Decodable {
|
||||||
public let domainCount: Int?
|
public let domainCount: Int?
|
||||||
public let statusCount: Int?
|
public let statusCount: Int?
|
||||||
public let userCount: Int?
|
public let userCount: Int?
|
||||||
|
@ -85,3 +99,68 @@ extension Instance {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Instance {
|
||||||
|
public struct Configuration: Decodable {
|
||||||
|
public let statuses: StatusesConfiguration
|
||||||
|
public let mediaAttachments: MediaAttachmentsConfiguration
|
||||||
|
/// Use Instance.pollsConfiguration to support older instance that don't have this nested
|
||||||
|
let polls: PollsConfiguration
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case statuses
|
||||||
|
case mediaAttachments = "media_attachments"
|
||||||
|
case polls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Instance {
|
||||||
|
public struct StatusesConfiguration: Decodable {
|
||||||
|
public let maxCharacters: Int
|
||||||
|
public let maxMediaAttachments: Int
|
||||||
|
public let charactersReservedPerURL: Int
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case maxCharacters = "max_characters"
|
||||||
|
case maxMediaAttachments = "max_media_attachments"
|
||||||
|
case charactersReservedPerURL = "characters_reserved_per_url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Instance {
|
||||||
|
public struct MediaAttachmentsConfiguration: Decodable {
|
||||||
|
public let supportedMIMETypes: [String]
|
||||||
|
public let imageSizeLimit: Int
|
||||||
|
public let imageMatrixLimit: Int
|
||||||
|
public let videoSizeLimit: Int
|
||||||
|
public let videoFrameRateLimit: Int
|
||||||
|
public let videoMatrixLimit: Int
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case supportedMIMETypes = "supported_mime_types"
|
||||||
|
case imageSizeLimit = "image_size_limit"
|
||||||
|
case imageMatrixLimit = "image_matrix_limit"
|
||||||
|
case videoSizeLimit = "video_size_limit"
|
||||||
|
case videoFrameRateLimit = "video_frame_rate_limit"
|
||||||
|
case videoMatrixLimit = "video_matrix_limit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Instance {
|
||||||
|
public struct PollsConfiguration: Decodable {
|
||||||
|
public let maxOptions: Int
|
||||||
|
public let maxCharactersPerOption: Int
|
||||||
|
public let minExpiration: TimeInterval
|
||||||
|
public let maxExpiration: TimeInterval
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case maxOptions = "max_options"
|
||||||
|
case maxCharactersPerOption = "max_characters_per_option"
|
||||||
|
case minExpiration = "min_expiration"
|
||||||
|
case maxExpiration = "max_expiration"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class NotificationGroup {
|
public class NotificationGroup: Identifiable, Hashable {
|
||||||
public let notifications: [Notification]
|
public let notifications: [Notification]
|
||||||
public let id: String
|
public let id: String
|
||||||
public let kind: Notification.Kind
|
public let kind: Notification.Kind
|
||||||
|
@ -26,6 +26,14 @@ public class NotificationGroup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {
|
||||||
|
return lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
|
||||||
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
||||||
var groups = [[Notification]]()
|
var groups = [[Notification]]()
|
||||||
for notification in notifications {
|
for notification in notifications {
|
||||||
|
@ -50,5 +58,3 @@ public class NotificationGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationGroup: Identifiable {}
|
|
||||||
|
|
|
@ -134,11 +134,16 @@
|
||||||
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 */; };
|
||||||
|
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */; };
|
||||||
|
D6420AEF26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */; };
|
||||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
||||||
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||||
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||||
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
||||||
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; };
|
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; };
|
||||||
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18723C1640A000D0238 /* PinStatusActivity.swift */; };
|
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18723C1640A000D0238 /* PinStatusActivity.swift */; };
|
||||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
|
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
|
||||||
|
@ -155,6 +160,7 @@
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
||||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.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 */; };
|
||||||
|
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */; };
|
||||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
|
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
|
||||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
|
||||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
|
||||||
|
@ -304,6 +310,8 @@
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
||||||
|
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */; };
|
||||||
|
D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */; };
|
||||||
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 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
|
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
|
||||||
|
@ -532,11 +540,16 @@
|
||||||
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>"; };
|
||||||
|
D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineDescriptionTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PublicTimelineDescriptionTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
||||||
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
||||||
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
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>"; };
|
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
||||||
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
|
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
|
||||||
D64BC18723C1640A000D0238 /* PinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinStatusActivity.swift; sourceTree = "<group>"; };
|
D64BC18723C1640A000D0238 /* PinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinStatusActivity.swift; sourceTree = "<group>"; };
|
||||||
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
|
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
|
||||||
|
@ -553,6 +566,7 @@
|
||||||
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.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>"; };
|
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>"; };
|
||||||
|
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableTimelineLikeTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; };
|
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; };
|
||||||
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -707,6 +721,8 @@
|
||||||
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
||||||
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
||||||
|
D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfirmLoadMoreTableViewCell.xib; 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 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
|
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1052,27 +1068,27 @@
|
||||||
D641C780213DD7C4004B4513 /* Screens */ = {
|
D641C780213DD7C4004B4513 /* Screens */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
|
||||||
D641C783213DD7FE004B4513 /* Onboarding */,
|
|
||||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
|
||||||
D641C781213DD7DD004B4513 /* Timeline */,
|
|
||||||
D641C784213DD819004B4513 /* Profile */,
|
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
|
||||||
D641C786213DD852004B4513 /* Notifications */,
|
|
||||||
D641C787213DD862004B4513 /* Compose */,
|
|
||||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||||
|
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||||
|
D627944823A6AD5100D38C68 /* Bookmarks */,
|
||||||
|
D641C787213DD862004B4513 /* Compose */,
|
||||||
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
D627FF77217E94F200CC0648 /* Drafts */,
|
D627FF77217E94F200CC0648 /* Drafts */,
|
||||||
D627943C23A5635D00D38C68 /* Explore */,
|
D627943C23A5635D00D38C68 /* Explore */,
|
||||||
D6BC9DD8232D8BCA002CA326 /* Search */,
|
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||||
D627944B23A9A02400D38C68 /* Lists */,
|
|
||||||
D641C788213DD86D004B4513 /* Large Image */,
|
D641C788213DD86D004B4513 /* Large Image */,
|
||||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
D627944B23A9A02400D38C68 /* Lists */,
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
D641C786213DD852004B4513 /* Notifications */,
|
||||||
D627944823A6AD5100D38C68 /* Bookmarks */,
|
D641C783213DD7FE004B4513 /* Onboarding */,
|
||||||
D641C789213DD87E004B4513 /* Preferences */,
|
D641C789213DD87E004B4513 /* Preferences */,
|
||||||
|
D641C784213DD819004B4513 /* Profile */,
|
||||||
|
D6BC9DD8232D8BCA002CA326 /* Search */,
|
||||||
|
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
||||||
|
D641C781213DD7DD004B4513 /* Timeline */,
|
||||||
|
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
||||||
);
|
);
|
||||||
path = Screens;
|
path = Screens;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1234,6 +1250,15 @@
|
||||||
path = Notifications;
|
path = Notifications;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6420AEB26BED17500ED8175 /* Timeline Description Cell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */,
|
||||||
|
D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */,
|
||||||
|
);
|
||||||
|
path = "Timeline Description Cell";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D646C954213B364600269FB5 /* Transitions */ = {
|
D646C954213B364600269FB5 /* Transitions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1244,6 +1269,16 @@
|
||||||
path = Transitions;
|
path = Transitions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D64AAE8F26C80DB600FC57FB /* Toast */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */,
|
||||||
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */,
|
||||||
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Toast;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D65A37F221472F300087646E /* Frameworks */ = {
|
D65A37F221472F300087646E /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1435,34 +1470,37 @@
|
||||||
D6BED1722126661300F02DA0 /* Views */ = {
|
D6BED1722126661300F02DA0 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */,
|
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */,
|
|
||||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
||||||
|
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||||
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
||||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
||||||
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
||||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
||||||
|
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
||||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
D620483723D38190008A63EF /* StatusContentTextView.swift */,
|
||||||
|
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
|
||||||
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||||
D626494023C122C800612E6E /* Asset Picker */,
|
D626494023C122C800612E6E /* Asset Picker */,
|
||||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
|
||||||
D641C78A213DD926004B4513 /* Status */,
|
|
||||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||||
D623A53B2635F4E20095BD04 /* Poll */,
|
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||||
D641C78C213DD937004B4513 /* Notifications */,
|
|
||||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
|
||||||
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
|
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
|
||||||
D61AC1DA232EA43100C54D2D /* Instance Cell */,
|
D61AC1DA232EA43100C54D2D /* Instance Cell */,
|
||||||
|
D641C78C213DD937004B4513 /* Notifications */,
|
||||||
|
D623A53B2635F4E20095BD04 /* Poll */,
|
||||||
|
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||||
|
D641C78A213DD926004B4513 /* Status */,
|
||||||
|
D64AAE8F26C80DB600FC57FB /* Toast */,
|
||||||
|
D6420AEB26BED17500ED8175 /* Timeline Description Cell */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1470,23 +1508,24 @@
|
||||||
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
|
||||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
|
||||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
|
||||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
|
||||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||||
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||||
|
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||||
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||||
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
||||||
|
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
||||||
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||||
|
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
||||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
||||||
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||||
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||||
|
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||||
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||||
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1533,36 +1572,36 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */,
|
D6E4885C24A2890C0011C13E /* Tusker.entitlements */,
|
||||||
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
|
||||||
|
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
||||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
|
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
|
||||||
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
|
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
|
||||||
|
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||||
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
||||||
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||||
|
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
||||||
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
|
||||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
|
||||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
|
||||||
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
|
||||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
|
||||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
|
||||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
|
||||||
D663626021360A9600C9CBA2 /* Preferences */,
|
|
||||||
D6AEBB3F2321640F00E5038B /* Activities */,
|
|
||||||
D667E5F62135C2ED0057A976 /* Extensions */,
|
|
||||||
D61959D2241E846D00A37B8E /* Models */,
|
|
||||||
D6370B9924421FE00092A7FF /* CoreData */,
|
|
||||||
D6F953F121251A2F00CF0F2B /* Controllers */,
|
|
||||||
D641C780213DD7C4004B4513 /* Screens */,
|
|
||||||
D6BED1722126661300F02DA0 /* Views */,
|
|
||||||
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
||||||
|
D6AEBB3F2321640F00E5038B /* Activities */,
|
||||||
|
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||||
|
D6F953F121251A2F00CF0F2B /* Controllers */,
|
||||||
|
D6370B9924421FE00092A7FF /* CoreData */,
|
||||||
|
D667E5F62135C2ED0057A976 /* Extensions */,
|
||||||
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
|
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
|
||||||
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
|
D61959D2241E846D00A37B8E /* Models */,
|
||||||
|
D663626021360A9600C9CBA2 /* Preferences */,
|
||||||
|
D641C780213DD7C4004B4513 /* Screens */,
|
||||||
|
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||||
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
|
D6BED1722126661300F02DA0 /* Views */,
|
||||||
|
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||||
);
|
);
|
||||||
path = Tusker;
|
path = Tusker;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1592,6 +1631,15 @@
|
||||||
path = TuskerUITests;
|
path = TuskerUITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */,
|
||||||
|
D6DEA0DD268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib */,
|
||||||
|
);
|
||||||
|
path = "Confirm Load More Cell";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */ = {
|
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1869,6 +1917,7 @@
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||||
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
||||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||||
|
D6420AEF26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib in Resources */,
|
||||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||||
|
@ -1886,6 +1935,7 @@
|
||||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||||
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
|
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
|
||||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
||||||
|
D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -2019,6 +2069,7 @@
|
||||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
||||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||||
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
|
D6093FB725BE0CF3004811E6 /* HashtagHistoryView.swift in Sources */,
|
||||||
|
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */,
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||||
|
@ -2033,6 +2084,7 @@
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
||||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||||
|
@ -2114,6 +2166,7 @@
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||||
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
|
@ -2128,6 +2181,8 @@
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
||||||
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
||||||
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
||||||
|
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||||
|
@ -2151,6 +2206,7 @@
|
||||||
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
|
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
|
||||||
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
|
@ -2489,6 +2545,7 @@
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
@ -2545,6 +2602,7 @@
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
@ -2557,7 +2615,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 19;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2573,6 +2631,10 @@
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SUPPORTS_MACCATALYST = YES;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphoneos15.0]" = "SDK_IOS_15 $(inherited)";
|
||||||
|
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphonesimulator15.0]" = "SDK_IOS_15 $(inherited)";
|
||||||
|
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=macosx12.0]" = "SDK_IOS_15 $(inherited)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||||
};
|
};
|
||||||
|
@ -2586,7 +2648,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 19;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2695,7 +2757,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 19;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2722,7 +2784,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 19;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
|
|
@ -88,6 +88,10 @@
|
||||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-UIFocusLoopDebuggerEnabled YES"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</CommandLineArgument>
|
||||||
</CommandLineArguments>
|
</CommandLineArguments>
|
||||||
<EnvironmentVariables>
|
<EnvironmentVariables>
|
||||||
<EnvironmentVariable
|
<EnvironmentVariable
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"object": {
|
"object": {
|
||||||
"pins": [
|
"pins": [
|
||||||
{
|
{
|
||||||
"package": "PLCrashReporter",
|
"package": "plcrashreporter",
|
||||||
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
|
|
|
@ -17,21 +17,39 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard LocalData.shared.onboardingComplete else {
|
||||||
|
UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let account: LocalData.UserAccountInfo
|
let account: LocalData.UserAccountInfo
|
||||||
|
let controller: MastodonController
|
||||||
let draft: Draft?
|
let draft: Draft?
|
||||||
|
|
||||||
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity,
|
if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||||
let activityAccount = UserActivityManager.getAccount(from: activity) {
|
if let activityAccount = UserActivityManager.getAccount(from: activity) {
|
||||||
account = activityAccount
|
account = activityAccount
|
||||||
draft = UserActivityManager.getDraft(from: activity)
|
} else {
|
||||||
|
// todo: this potentially changes the account for the draft, should show the same warning to user as in the drafts selection screen
|
||||||
|
account = LocalData.shared.getMostRecentAccount()!
|
||||||
|
}
|
||||||
|
|
||||||
|
controller = MastodonController.getForAccount(account)
|
||||||
|
|
||||||
|
if let activityDraft = UserActivityManager.getDraft(from: activity) {
|
||||||
|
draft = activityDraft
|
||||||
|
} else if let mentioning = activity.userInfo?["mentioning"] as? String {
|
||||||
|
draft = controller.createDraft(inReplyToID: nil, mentioningAcct: mentioning)
|
||||||
|
} else {
|
||||||
|
draft = nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
account = LocalData.shared.getMostRecentAccount()!
|
account = LocalData.shared.getMostRecentAccount()!
|
||||||
|
controller = MastodonController.getForAccount(account)
|
||||||
draft = nil
|
draft = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = MastodonController.getForAccount(account)
|
|
||||||
session.mastodonController = controller
|
session.mastodonController = controller
|
||||||
|
|
||||||
controller.getOwnAccount()
|
controller.getOwnAccount()
|
||||||
controller.getOwnInstance()
|
controller.getOwnInstance()
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import MobileCoreServices
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
||||||
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
||||||
|
@ -52,10 +52,10 @@ final class CompositionAttachment: NSObject, Codable, ObservableObject {
|
||||||
|
|
||||||
extension CompositionAttachment: Identifiable {}
|
extension CompositionAttachment: Identifiable {}
|
||||||
|
|
||||||
private let imageType = kUTTypeImage as String
|
private let imageType = UTType.image.identifier
|
||||||
private let mp4Type = kUTTypeMPEG4 as String
|
private let mp4Type = UTType.mpeg4Movie.identifier
|
||||||
private let quickTimeType = kUTTypeQuickTimeMovie as String
|
private let quickTimeType = UTType.quickTimeMovie.identifier
|
||||||
private let dataType = kUTTypeData as String
|
private let dataType = UTType.data.identifier
|
||||||
|
|
||||||
extension CompositionAttachment: NSItemProviderWriting {
|
extension CompositionAttachment: NSItemProviderWriting {
|
||||||
static var writableTypeIdentifiersForItemProvider: [String] {
|
static var writableTypeIdentifiersForItemProvider: [String] {
|
||||||
|
@ -100,11 +100,11 @@ extension CompositionAttachment: NSItemProviderReading {
|
||||||
return try PropertyListDecoder().decode(Self.self, from: data)
|
return try PropertyListDecoder().decode(Self.self, from: data)
|
||||||
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
|
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
|
||||||
return CompositionAttachment(data: .image(image)) as! Self
|
return CompositionAttachment(data: .image(image)) as! Self
|
||||||
} else if typeIdentifier == mp4Type || typeIdentifier == quickTimeType {
|
} else if let type = UTType(typeIdentifier), type == .mpeg4Movie || type == .quickTimeMovie {
|
||||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
let temporaryFileName = ProcessInfo().globallyUniqueString
|
let temporaryFileName = ProcessInfo().globallyUniqueString
|
||||||
let fileExt = UTTypeCopyPreferredTagWithClass(typeIdentifier as CFString, kUTTagClassFilenameExtension)!
|
let fileExt = type.preferredFilenameExtension!
|
||||||
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt.takeUnretainedValue() as String)
|
let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt)
|
||||||
try data.write(to: temporaryFileURL)
|
try data.write(to: temporaryFileURL)
|
||||||
return CompositionAttachment(data: .video(temporaryFileURL)) as! Self
|
return CompositionAttachment(data: .video(temporaryFileURL)) as! Self
|
||||||
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {
|
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Photos
|
import Photos
|
||||||
import MobileCoreServices
|
import UniformTypeIdentifiers
|
||||||
import PencilKit
|
import PencilKit
|
||||||
|
|
||||||
enum CompositionAttachmentData {
|
enum CompositionAttachmentData {
|
||||||
|
@ -74,7 +74,7 @@ enum CompositionAttachmentData {
|
||||||
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
data = context.jpegRepresentation(of: image, colorSpace: colorSpace, options: [:])!
|
||||||
mimeType = "image/jpeg"
|
mimeType = "image/jpeg"
|
||||||
} else {
|
} else {
|
||||||
mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)!.takeRetainedValue() as String
|
mimeType = UTType(dataUTI)!.preferredMIMEType!
|
||||||
}
|
}
|
||||||
|
|
||||||
completion(data, mimeType)
|
completion(data, mimeType)
|
||||||
|
|
|
@ -63,9 +63,13 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
||||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||||
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
||||||
|
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
||||||
|
|
||||||
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
||||||
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
||||||
|
|
||||||
|
self.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false
|
||||||
|
self.hasShownFederatedTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownFederatedTimelineDescription) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
|
@ -97,9 +101,13 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
||||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||||
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
||||||
|
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
||||||
|
|
||||||
try container.encode(silentActions, forKey: .silentActions)
|
try container.encode(silentActions, forKey: .silentActions)
|
||||||
try container.encode(statusContentType, forKey: .statusContentType)
|
try container.encode(statusContentType, forKey: .statusContentType)
|
||||||
|
|
||||||
|
try container.encode(hasShownLocalTimelineDescription, forKey: .hasShownLocalTimelineDescription)
|
||||||
|
try container.encode(hasShownFederatedTimelineDescription, forKey: .hasShownFederatedTimelineDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Appearance
|
// MARK: Appearance
|
||||||
|
@ -133,11 +141,16 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var showFavoriteAndReblogCounts = true
|
@Published var showFavoriteAndReblogCounts = true
|
||||||
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
||||||
@Published var grayscaleImages = false
|
@Published var grayscaleImages = false
|
||||||
|
@Published var disableInfiniteScrolling = false
|
||||||
|
|
||||||
// MARK: Advanced
|
// MARK: Advanced
|
||||||
@Published var silentActions: [String: Permission] = [:]
|
@Published var silentActions: [String: Permission] = [:]
|
||||||
@Published var statusContentType: StatusContentType = .plain
|
@Published var statusContentType: StatusContentType = .plain
|
||||||
|
|
||||||
|
// MARK:
|
||||||
|
@Published var hasShownLocalTimelineDescription = false
|
||||||
|
@Published var hasShownFederatedTimelineDescription = false
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case theme
|
case theme
|
||||||
case avatarStyle
|
case avatarStyle
|
||||||
|
@ -165,9 +178,13 @@ class Preferences: Codable, ObservableObject {
|
||||||
case showFavoriteAndReblogCounts
|
case showFavoriteAndReblogCounts
|
||||||
case defaultNotificationsType
|
case defaultNotificationsType
|
||||||
case grayscaleImages
|
case grayscaleImages
|
||||||
|
case disableInfiniteScrolling
|
||||||
|
|
||||||
case silentActions
|
case silentActions
|
||||||
case statusContentType
|
case statusContentType
|
||||||
|
|
||||||
|
case hasShownLocalTimelineDescription
|
||||||
|
case hasShownFederatedTimelineDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
|
|
||||||
collectionView.alwaysBounceVertical = true
|
collectionView.alwaysBounceVertical = true
|
||||||
collectionView.allowsMultipleSelection = true
|
collectionView.allowsMultipleSelection = true
|
||||||
|
collectionView.allowsSelection = true
|
||||||
|
|
||||||
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
||||||
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
||||||
|
@ -98,8 +99,6 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setEditing(true, animated: false)
|
|
||||||
|
|
||||||
updateItemsSelectedCount()
|
updateItemsSelectedCount()
|
||||||
|
|
||||||
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {
|
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {
|
||||||
|
|
|
@ -51,8 +51,14 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
}
|
}
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
|
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||||
|
didSet {
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
override var prefersStatusBarHidden: Bool {
|
||||||
return true
|
return !isInteractivelyAnimatingDismissal
|
||||||
}
|
}
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||||
return .none
|
return .none
|
||||||
|
|
|
@ -41,6 +41,16 @@ struct ComposeAttachmentRow: View {
|
||||||
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
Button(action: self.removeAttachment) {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}.foregroundStyle(.red)
|
||||||
|
} else {
|
||||||
|
Button(action: self.removeAttachment) {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
|
@ -126,7 +136,9 @@ struct ComposeAttachmentRow: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeAttachment() {
|
private func removeAttachment() {
|
||||||
draft.attachments.removeAll { $0.id == attachment.id }
|
withAnimation {
|
||||||
|
draft.attachments.removeAll { $0.id == attachment.id }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func editDrawing() {
|
private func editDrawing() {
|
||||||
|
|
|
@ -69,6 +69,7 @@ struct ComposeAttachmentsList: View {
|
||||||
.frame(height: cellHeight / 2)
|
.frame(height: cellHeight / 2)
|
||||||
.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))
|
||||||
}
|
}
|
||||||
|
.listStyle(PlainListStyle())
|
||||||
.frame(height: totalListHeight)
|
.frame(height: totalListHeight)
|
||||||
.onAppear(perform: self.didAppear)
|
.onAppear(perform: self.didAppear)
|
||||||
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
.onReceive(draft.$attachments, perform: self.attachmentsChanged)
|
||||||
|
|
|
@ -80,12 +80,13 @@ class ComposeDrawingViewController: UIViewController {
|
||||||
|
|
||||||
updateLayout(for: toolPicker)
|
updateLayout(for: toolPicker)
|
||||||
canvasView.becomeFirstResponder()
|
canvasView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
// wait until the next run loop iteration so that the canvas view has become first responder and it's undo manager exists
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
DispatchQueue.main.async {
|
super.viewDidAppear(animated)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidUndoChange, object: self.undoManager!)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidRedoChange, object: self.undoManager!)
|
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidUndoChange, object: self.undoManager!)
|
||||||
}
|
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidRedoChange, object: self.undoManager!)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(for toolPicker: PKToolPicker) {
|
func updateLayout(for toolPicker: PKToolPicker) {
|
||||||
|
|
|
@ -245,6 +245,25 @@ extension ComposeHostingController: ComposeUIStateDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentAssetPickerSheet() {
|
func presentAssetPickerSheet() {
|
||||||
|
#if SDK_IOS_15
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
let picker = AssetPickerViewController()
|
||||||
|
picker.assetPickerDelegate = self
|
||||||
|
picker.modalPresentationStyle = .pageSheet
|
||||||
|
picker.overrideUserInterfaceStyle = .dark
|
||||||
|
let sheet = picker.sheetPresentationController!
|
||||||
|
sheet.detents = [.medium(), .large()]
|
||||||
|
sheet.prefersEdgeAttachedInCompactHeight = true
|
||||||
|
self.present(picker, animated: true)
|
||||||
|
} else {
|
||||||
|
presentOldAssetPickerSheet()
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
presentOldAssetPickerSheet()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentOldAssetPickerSheet() {
|
||||||
let sheetContainer = AssetPickerSheetContainerViewController()
|
let sheetContainer = AssetPickerSheetContainerViewController()
|
||||||
sheetContainer.assetPicker.assetPickerDelegate = self
|
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||||
self.present(sheetContainer, animated: true)
|
self.present(sheetContainer, animated: true)
|
||||||
|
|
|
@ -46,6 +46,7 @@ struct ComposePollView: View {
|
||||||
}
|
}
|
||||||
.accentColor(buttonForegroundColor)
|
.accentColor(buttonForegroundColor)
|
||||||
.background(Circle().foregroundColor(buttonBackgroundColor))
|
.background(Circle().foregroundColor(buttonBackgroundColor))
|
||||||
|
.hoverEffect()
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
|
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
|
||||||
|
@ -161,6 +162,7 @@ struct ComposePollOption: View {
|
||||||
}
|
}
|
||||||
.foregroundColor(poll.options.count == 1 ? .gray : .red)
|
.foregroundColor(poll.options.count == 1 ? .gray : .red)
|
||||||
.disabled(poll.options.count == 1)
|
.disabled(poll.options.count == 1)
|
||||||
|
.hoverEffect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,9 +52,20 @@ struct ComposeReplyView: View {
|
||||||
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
|
||||||
// using named coordinate spaces produces an incorrect scroll offset on iOS 13,
|
// using named coordinate spaces produces an incorrect scroll offset on iOS 13,
|
||||||
// so simply compare the geometry inside and outside the scroll view in the global coordinate space
|
// so simply compare the geometry inside and outside the scroll view in the global coordinate space
|
||||||
var scrollOffset = outerMinY - geometry.frame(in: .global).minY
|
let scrollOffset = outerMinY - geometry.frame(in: .global).minY
|
||||||
scrollOffset += stackPadding
|
|
||||||
let offset = min(max(scrollOffset, 0), geometry.size.height - 50 - stackPadding)
|
// add stackPadding so that the image is always at least stackPadding away from the top
|
||||||
|
var offset = scrollOffset + stackPadding
|
||||||
|
|
||||||
|
// offset can never be less than 0 (i.e., above the top of the in-reply-to content)
|
||||||
|
offset = max(offset, 0)
|
||||||
|
|
||||||
|
// subtract 50, because we care about where the bottom of the view is but the offset is relative to the top of the view
|
||||||
|
let maxOffset = (contentHeight ?? 0) - 50
|
||||||
|
|
||||||
|
// once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content
|
||||||
|
offset = min(offset, maxOffset)
|
||||||
|
|
||||||
return ComposeAvatarImageView(url: status.account.avatar)
|
return ComposeAvatarImageView(url: status.account.avatar)
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
||||||
|
|
|
@ -58,8 +58,10 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
if postProgress > 0 {
|
||||||
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||||
|
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
||||||
|
}
|
||||||
|
|
||||||
autocompleteSuggestions
|
autocompleteSuggestions
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,6 +132,30 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
visibilityBarButtonItem = UIBarButtonItem(image: initialImage, style: .plain, target: self, action: #selector(toggleVisibilityButtonPressed))
|
||||||
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
navigationItem.rightBarButtonItem = visibilityBarButtonItem
|
||||||
|
|
||||||
|
loadMainStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadMainStatus() {
|
||||||
|
if let mainStatus = mastodonController.persistentContainer.status(for: mainStatusID) {
|
||||||
|
self.mainStatusLoaded(mainStatus)
|
||||||
|
} else {
|
||||||
|
let request = Client.getStatus(id: mainStatusID)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .success(status, _):
|
||||||
|
let viewContext = self.mastodonController.persistentContainer.viewContext
|
||||||
|
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false, context: viewContext) { (statusMO) in
|
||||||
|
self.mainStatusLoaded(statusMO)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(_):
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mainStatusLoaded(_ mainStatus: StatusMO) {
|
||||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
@ -139,12 +163,10 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
guard let mainStatus = self.mastodonController.persistentContainer.status(for: self.mainStatusID) else {
|
|
||||||
fatalError("Missing cached status \(self.mainStatusID)")
|
|
||||||
}
|
|
||||||
let mainStatusInReplyToID = mainStatus.inReplyToID
|
let mainStatusInReplyToID = mainStatus.inReplyToID
|
||||||
mainStatus.incrementReferenceCount()
|
mainStatus.incrementReferenceCount()
|
||||||
|
|
||||||
|
// todo: it would be nice to cache these contexts
|
||||||
let request = Status.getContext(mainStatusID)
|
let request = Status.getContext(mainStatusID)
|
||||||
mastodonController.run(request) { response in
|
mastodonController.run(request) { response in
|
||||||
guard case let .success(context, _) = response else { fatalError() }
|
guard case let .success(context, _) = response else { fatalError() }
|
||||||
|
@ -304,7 +326,16 @@ class ConversationTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func toggleVisibilityButtonPressed() {
|
@objc func toggleVisibilityButtonPressed() {
|
||||||
|
#if SDK_IOS_15
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
visibilityBarButtonItem.isSelected = !visibilityBarButtonItem.isSelected
|
||||||
|
showStatusesAutomatically = visibilityBarButtonItem.isSelected
|
||||||
|
} else {
|
||||||
|
showStatusesAutomatically = !showStatusesAutomatically
|
||||||
|
}
|
||||||
|
#else
|
||||||
showStatusesAutomatically = !showStatusesAutomatically
|
showStatusesAutomatically = !showStatusesAutomatically
|
||||||
|
#endif
|
||||||
|
|
||||||
let snapshot = dataSource.snapshot()
|
let snapshot = dataSource.snapshot()
|
||||||
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
||||||
|
|
|
@ -73,12 +73,12 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
accountView.alpha = 0
|
accountView.alpha = 0
|
||||||
accountView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
|
accountView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
|
||||||
UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: relDuration) {
|
UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: relDuration / 2) {
|
||||||
accountView.alpha = 1
|
accountView.alpha = 1
|
||||||
accountView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
accountView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||||
}
|
}
|
||||||
|
|
||||||
UIView.addKeyframe(withRelativeStartTime: relStart + relDuration, relativeDuration: relDuration) {
|
UIView.addKeyframe(withRelativeStartTime: relStart + relDuration / 2, relativeDuration: relDuration / 2) {
|
||||||
accountView.transform = .identity
|
accountView.transform = .identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,8 +51,14 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
private var prevZoomScale: CGFloat?
|
private var prevZoomScale: CGFloat?
|
||||||
private var isGrayscale = false
|
private var isGrayscale = false
|
||||||
|
|
||||||
|
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||||
|
didSet {
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
override var prefersStatusBarHidden: Bool {
|
||||||
return true
|
return !isInteractivelyAnimatingDismissal
|
||||||
}
|
}
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||||
return .none
|
return .none
|
||||||
|
@ -137,11 +143,14 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
let notchedDeviceTopInsets: [CGFloat] = [
|
let notchedDeviceTopInsets: [CGFloat] = [
|
||||||
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
|
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
|
||||||
48, // iPhone XR, 11
|
48, // iPhone XR, 11
|
||||||
47, // iPhone 12, 12 Pro, 12 Pro Max
|
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max
|
||||||
50, // iPhone 12 mini
|
50, // iPhone 12 mini, 13 mini
|
||||||
]
|
]
|
||||||
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
||||||
let notchWidth: CGFloat = 209
|
// 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 earWidth = (view.bounds.width - notchWidth) / 2
|
||||||
let offset = (earWidth - shareButton.bounds.width) / 2
|
let offset = (earWidth - shareButton.bounds.width) / 2
|
||||||
shareButtonLeadingConstraint.constant = offset
|
shareButtonLeadingConstraint.constant = offset
|
||||||
|
|
|
@ -43,8 +43,14 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
var animationGifData: Data? { largeImageVC?.animationGifData }
|
var animationGifData: Data? { largeImageVC?.animationGifData }
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
var dismissInteractionController: LargeImageInteractionController?
|
||||||
|
|
||||||
|
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||||
|
didSet {
|
||||||
|
setNeedsStatusBarAppearanceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
override var prefersStatusBarHidden: Bool {
|
||||||
return true
|
return !isInteractivelyAnimatingDismissal
|
||||||
}
|
}
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||||
return .none
|
return .none
|
||||||
|
|
|
@ -15,6 +15,7 @@ protocol LargeImageAnimatableViewController: UIViewController {
|
||||||
var animationImage: UIImage? { get }
|
var animationImage: UIImage? { get }
|
||||||
var animationGifData: Data? { get }
|
var animationGifData: Data? { get }
|
||||||
var dismissInteractionController: LargeImageInteractionController? { get }
|
var dismissInteractionController: LargeImageInteractionController? { get }
|
||||||
|
var isInteractivelyAnimatingDismissal: Bool { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LargeImageAnimatableViewController {
|
extension LargeImageAnimatableViewController {
|
||||||
|
@ -74,7 +75,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
toVC.largeImageController?.contentView.isHidden = true
|
toVC.largeImageController?.contentView.isHidden = true
|
||||||
toVC.largeImageController?.setControlsVisible(false, animated: false)
|
toVC.largeImageController?.setControlsVisible(false, animated: false)
|
||||||
|
|
||||||
var finalFrameSize = finalVCFrame.inset(by: fromVC.view.safeAreaInsets).size
|
var finalFrameSize = finalVCFrame.inset(by: toVC.view.safeAreaInsets).size
|
||||||
let newWidth = finalFrameSize.width / image.size.width
|
let newWidth = finalFrameSize.width / image.size.width
|
||||||
let newHeight = finalFrameSize.height / image.size.height
|
let newHeight = finalFrameSize.height / image.size.height
|
||||||
if newHeight < newWidth {
|
if newHeight < newWidth {
|
||||||
|
|
|
@ -14,9 +14,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
||||||
var direction: CGFloat?
|
var direction: CGFloat?
|
||||||
|
|
||||||
var shouldCompleteTransition = false
|
var shouldCompleteTransition = false
|
||||||
private weak var viewController: UIViewController!
|
private weak var viewController: LargeImageAnimatableViewController!
|
||||||
|
|
||||||
init(viewController: UIViewController) {
|
init(viewController: LargeImageAnimatableViewController) {
|
||||||
super.init()
|
super.init()
|
||||||
self.viewController = viewController
|
self.viewController = viewController
|
||||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
||||||
|
@ -42,6 +42,7 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
||||||
viewController.dismiss(animated: true)
|
viewController.dismiss(animated: true)
|
||||||
case .changed:
|
case .changed:
|
||||||
shouldCompleteTransition = progress > 0.5 || velocity > 1000
|
shouldCompleteTransition = progress > 0.5 || velocity > 1000
|
||||||
|
viewController.isInteractivelyAnimatingDismissal = progress > 0.1
|
||||||
update(progress)
|
update(progress)
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
inProgress = false
|
inProgress = false
|
||||||
|
@ -59,4 +60,9 @@ class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func cancel() {
|
||||||
|
super.cancel()
|
||||||
|
viewController.isInteractivelyAnimatingDismissal = false
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,15 +80,15 @@ class EditListAccountsViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
self.nextRange = pagination?.older
|
self.nextRange = pagination?.older
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(accounts: accounts)
|
self.mastodonController.persistentContainer.addAll(accounts: accounts) {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.accounts])
|
||||||
|
snapshot.appendSections([.accounts])
|
||||||
|
snapshot.appendItems(accounts.map { .account(id: $0.id) })
|
||||||
|
|
||||||
var snapshot = self.dataSource.snapshot()
|
DispatchQueue.main.async {
|
||||||
snapshot.deleteSections([.accounts])
|
self.dataSource.apply(snapshot)
|
||||||
snapshot.appendSections([.accounts])
|
}
|
||||||
snapshot.appendItems(accounts.map { .account(id: $0.id) })
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.dataSource.apply(snapshot)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ class ListTimelineViewController: TimelineTableViewController {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
|
|
||||||
// todo: show loading indicator
|
// todo: show loading indicator
|
||||||
reloadInitialItems()
|
reloadInitial()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,3 +89,11 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
root.performSearch(query: query)
|
root.performSearch(query: query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
||||||
|
func sceneDidEnterBackground() {
|
||||||
|
if let backgroundable = root as? BackgroundableViewController {
|
||||||
|
backgroundable.sceneDidEnterBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -448,6 +448,35 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 15.0, *)
|
||||||
|
func collectionView(_ collectionView: UICollectionView, selectionFollowsFocusForItemAt indexPath: IndexPath) -> Bool {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// don't immediately select items that present VCs when the they're focused, only when deliberately selected
|
||||||
|
switch item {
|
||||||
|
case .tab(.compose), .addList, .addSavedHashtag, .addSavedInstance:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
guard #available(iOS 15.0, *),
|
||||||
|
let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
let activity = userActivityForItem(item) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
||||||
|
return UIMenu(children: [
|
||||||
|
UIWindowScene.ActivationAction({ action in
|
||||||
|
return UIWindowScene.ActivationConfiguration(userActivity: activity)
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSidebarViewController: UICollectionViewDragDelegate {
|
extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||||
|
|
|
@ -353,10 +353,17 @@ fileprivate extension MainSidebarViewController.Item {
|
||||||
|
|
||||||
extension MainSplitViewController: TuskerRootViewController {
|
extension MainSplitViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
||||||
nav.presentationController?.delegate = vc
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
present(nav, animated: true)
|
options.preferredPresentationStyle = .prominent
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
||||||
|
} else {
|
||||||
|
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||||
|
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||||
|
nav.presentationController?.delegate = vc
|
||||||
|
present(nav, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(tab: MainTabBarViewController.Tab) {
|
func select(tab: MainTabBarViewController.Tab) {
|
||||||
|
|
|
@ -212,10 +212,17 @@ extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
|
|
||||||
extension MainTabBarViewController: TuskerRootViewController {
|
extension MainTabBarViewController: TuskerRootViewController {
|
||||||
@objc func presentCompose() {
|
@objc func presentCompose() {
|
||||||
let vc = ComposeHostingController(mastodonController: mastodonController)
|
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
let compose = UserActivityManager.newPostActivity(mentioning: nil, accountID: mastodonController.accountInfo!.id)
|
||||||
nav.presentationController?.delegate = vc
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
present(nav, animated: true)
|
options.preferredPresentationStyle = .prominent
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
||||||
|
} else {
|
||||||
|
let vc = ComposeHostingController(mastodonController: mastodonController)
|
||||||
|
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||||
|
nav.presentationController?.delegate = vc
|
||||||
|
present(nav, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(tab: Tab) {
|
func select(tab: Tab) {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class NotificationsTableViewController: TimelineLikeTableViewController<NotificationGroup> {
|
class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationGroup> {
|
||||||
|
|
||||||
private let statusCell = "statusCell"
|
private let statusCell = "statusCell"
|
||||||
private let actionGroupCell = "actionGroupCell"
|
private let actionGroupCell = "actionGroupCell"
|
||||||
|
@ -54,88 +54,9 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping ([NotificationGroup]) -> Void) {
|
// MARK: - DiffableTimelineLikeTableViewController
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(notifications, pagination) = response else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
|
||||||
self.older = pagination?.older
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
|
||||||
completion(groups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadOlder(completion: @escaping ([NotificationGroup]) -> Void) {
|
|
||||||
guard let older = older else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
|
||||||
|
|
||||||
self.older = pagination?.older
|
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
|
||||||
completion(groups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadNewer(completion: @escaping ([NotificationGroup]) -> Void) {
|
|
||||||
guard let newer = newer else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
|
||||||
|
|
||||||
if let newer = pagination?.newer {
|
|
||||||
self.newer = newer
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
|
||||||
completion(groups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
|
||||||
let group = DispatchGroup()
|
|
||||||
item(for: indexPath).notifications
|
|
||||||
.map { Pachyderm.Notification.dismiss(id: $0.id) }
|
|
||||||
.forEach { (request) in
|
|
||||||
group.enter()
|
|
||||||
mastodonController.run(request) { (_) in
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
group.notify(queue: .main) {
|
|
||||||
self.sections[indexPath.section].remove(at: indexPath.row)
|
|
||||||
self.tableView.deleteRows(at: [indexPath], with: .automatic)
|
|
||||||
completion?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let group = item(for: indexPath)
|
|
||||||
|
|
||||||
|
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ group: NotificationGroup) -> UITableViewCell? {
|
||||||
switch group.kind {
|
switch group.kind {
|
||||||
case .mention:
|
case .mention:
|
||||||
guard let notification = group.notifications.first,
|
guard let notification = group.notifications.first,
|
||||||
|
@ -179,6 +100,112 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
|
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(notifications, pagination):
|
||||||
|
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
self.newer = pagination?.newer
|
||||||
|
self.older = pagination?.older
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
||||||
|
var snapshot = Snapshot()
|
||||||
|
snapshot.appendSections([.notifications])
|
||||||
|
snapshot.appendItems(groups, toSection: .notifications)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
|
guard let older = older else {
|
||||||
|
completion(.failure(.noOlder))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(newNotifications, pagination):
|
||||||
|
if let older = pagination?.older {
|
||||||
|
self.older = older
|
||||||
|
}
|
||||||
|
|
||||||
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||||
|
var snapshot = currentSnapshot()
|
||||||
|
snapshot.appendItems(groups, toSection: .notifications)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
|
guard let newer = newer else {
|
||||||
|
completion(.failure(.noNewer))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(newNotifications, pagination):
|
||||||
|
guard !newNotifications.isEmpty else {
|
||||||
|
completion(.failure(.allCaughtUp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||||
|
var snapshot = currentSnapshot()
|
||||||
|
if let first = snapshot.itemIdentifiers(inSection: .notifications).first {
|
||||||
|
snapshot.insertItems(groups, beforeItem: first)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems(groups, toSection: .notifications)
|
||||||
|
}
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
let group = DispatchGroup()
|
||||||
|
item.notifications
|
||||||
|
.map { Pachyderm.Notification.dismiss(id: $0.id) }
|
||||||
|
.forEach { (request) in
|
||||||
|
group.enter()
|
||||||
|
mastodonController.run(request) { (_) in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteItems([item])
|
||||||
|
self.dataSource.apply(snapshot, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
@ -211,6 +238,12 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationsTableViewController {
|
||||||
|
enum Section: CaseIterable, Hashable {
|
||||||
|
case notifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension NotificationsTableViewController: TuskerNavigationDelegate {
|
extension NotificationsTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
@ -224,7 +257,8 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
||||||
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notification in item(for: indexPath).notifications {
|
guard let group = dataSource.itemIdentifier(for: indexPath) else { continue }
|
||||||
|
for notification in group.notifications {
|
||||||
ImageCache.avatars.fetchIfNotCached(notification.account.avatar)
|
ImageCache.avatars.fetchIfNotCached(notification.account.avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,7 +266,8 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notification in item(for: indexPath).notifications {
|
guard let group = dataSource.itemIdentifier(for: indexPath) else { continue }
|
||||||
|
for notification in group.notifications {
|
||||||
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
|
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
var urlHandler: AnyCancellable?
|
var urlHandler: AnyCancellable?
|
||||||
var currentQuery: String?
|
var currentQuery: String?
|
||||||
|
|
||||||
|
private var activityIndicator: UIActivityIndicatorView!
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
return .portrait
|
return .portrait
|
||||||
|
@ -50,10 +52,15 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
|
// disable transparent background when scrolled to top because it gets weird with animating table items in and out
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithDefaultBackground()
|
||||||
|
navigationItem.scrollEdgeAppearance = appearance
|
||||||
|
|
||||||
|
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.estimatedRowHeight = 120
|
tableView.estimatedRowHeight = 120
|
||||||
|
createActivityIndicatorHeader()
|
||||||
|
|
||||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -73,12 +80,19 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
searchController.obscuresBackgroundDuringPresentation = false
|
searchController.obscuresBackgroundDuringPresentation = false
|
||||||
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
urlHandler = urlCheckerSubject
|
urlHandler = urlCheckerSubject
|
||||||
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
|
||||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
.sink(receiveValue: updateSpecificInstance)
|
.map { [weak self] (s) -> String in
|
||||||
|
if !s.isEmpty {
|
||||||
|
self?.activityIndicator.startAnimating()
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
||||||
|
.sink { [weak self] in self?.updateSpecificInstance(domain: $0) }
|
||||||
|
|
||||||
loadRecommendedInstances()
|
loadRecommendedInstances()
|
||||||
}
|
}
|
||||||
|
@ -112,6 +126,8 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSpecificInstance(domain: String) {
|
private func updateSpecificInstance(domain: String) {
|
||||||
|
activityIndicator.startAnimating()
|
||||||
|
|
||||||
let components = parseURLComponents(input: domain)
|
let components = parseURLComponents(input: domain)
|
||||||
let url = components.url!
|
let url = components.url!
|
||||||
|
|
||||||
|
@ -120,16 +136,26 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
client.run(request) { (response) in
|
client.run(request) { (response) in
|
||||||
var snapshot = self.dataSource.snapshot()
|
var snapshot = self.dataSource.snapshot()
|
||||||
if snapshot.indexOfSection(.selected) != nil {
|
if snapshot.indexOfSection(.selected) != nil {
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))
|
snapshot.deleteSections([.selected])
|
||||||
}
|
}
|
||||||
|
|
||||||
if case let .success(instance, _) = response {
|
if case let .success(instance, _) = response {
|
||||||
if !snapshot.sectionIdentifiers.contains(.selected) {
|
if snapshot.indexOfSection(.recommendedInstances) != nil {
|
||||||
|
snapshot.insertSections([.selected], beforeSection: .recommendedInstances)
|
||||||
|
} else {
|
||||||
snapshot.appendSections([.selected])
|
snapshot.appendSections([.selected])
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot.appendItems([.selected(url, instance)], toSection: .selected)
|
snapshot.appendItems([.selected(url, instance)], toSection: .selected)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot) {
|
||||||
|
self.activityIndicator.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.activityIndicator.stopAnimating()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,14 +163,69 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
|
|
||||||
private func loadRecommendedInstances() {
|
private func loadRecommendedInstances() {
|
||||||
InstanceSelector.getInstances(category: nil) { (response) in
|
InstanceSelector.getInstances(category: nil) { (response) in
|
||||||
guard case let .success(instances, _) = response else { fatalError() }
|
DispatchQueue.main.async {
|
||||||
|
switch response {
|
||||||
self.recommendedInstances = instances
|
case let .failure(error):
|
||||||
self.filterRecommendedResults()
|
self.showRecommendationsError(error)
|
||||||
|
case let .success(instances, _):
|
||||||
|
self.recommendedInstances = instances
|
||||||
|
self.filterRecommendedResults()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterRecommendedResults() {
|
private func createActivityIndicatorHeader() {
|
||||||
|
let header = UITableViewHeaderFooterView()
|
||||||
|
header.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
header.contentView.backgroundColor = .secondarySystemBackground
|
||||||
|
|
||||||
|
activityIndicator = UIActivityIndicatorView(style: .large)
|
||||||
|
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
header.contentView.addSubview(activityIndicator)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
activityIndicator.centerXAnchor.constraint(equalTo: header.contentView.centerXAnchor),
|
||||||
|
activityIndicator.topAnchor.constraint(equalTo: header.contentView.topAnchor, constant: 4),
|
||||||
|
activityIndicator.bottomAnchor.constraint(equalTo: header.contentView.bottomAnchor, constant: -4),
|
||||||
|
])
|
||||||
|
|
||||||
|
let fittingSize = CGSize(width: tableView.bounds.width - (tableView.safeAreaInsets.left + tableView.safeAreaInsets.right), height: 0)
|
||||||
|
let size = header.systemLayoutSizeFitting(fittingSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
||||||
|
header.frame = CGRect(origin: .zero, size: size)
|
||||||
|
|
||||||
|
tableView.tableHeaderView = header
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showRecommendationsError(_ error: Client.Error) {
|
||||||
|
let footer = UITableViewHeaderFooterView()
|
||||||
|
footer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.font = .boldSystemFont(ofSize: 17)
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.text = "Could not fetch suggested instances: \(error.localizedDescription)"
|
||||||
|
|
||||||
|
footer.contentView.addSubview(label)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalToSystemSpacingAfter: footer.contentView.leadingAnchor, multiplier: 1),
|
||||||
|
footer.contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1),
|
||||||
|
label.topAnchor.constraint(equalTo: footer.contentView.topAnchor, constant: 8),
|
||||||
|
label.bottomAnchor.constraint(equalTo: footer.contentView.bottomAnchor, constant: 8),
|
||||||
|
])
|
||||||
|
|
||||||
|
let fittingSize = CGSize(width: tableView.bounds.width - (tableView.safeAreaInsets.left + tableView.safeAreaInsets.right), height: 0)
|
||||||
|
let size = footer.systemLayoutSizeFitting(fittingSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
||||||
|
footer.frame = CGRect(origin: .zero, size: size)
|
||||||
|
|
||||||
|
tableView.tableFooterView = footer
|
||||||
|
}
|
||||||
|
|
||||||
|
private func filterRecommendedResults() {
|
||||||
let filteredInstances: [InstanceSelector.Instance]
|
let filteredInstances: [InstanceSelector.Instance]
|
||||||
if let currentQuery = currentQuery, !currentQuery.isEmpty {
|
if let currentQuery = currentQuery, !currentQuery.isEmpty {
|
||||||
filteredInstances = recommendedInstances.filter {
|
filteredInstances = recommendedInstances.filter {
|
||||||
|
@ -155,12 +236,20 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = self.dataSource.snapshot()
|
var snapshot = self.dataSource.snapshot()
|
||||||
snapshot.deleteSections([.recommendedInstances])
|
if snapshot.indexOfSection(.recommendedInstances) != nil {
|
||||||
snapshot.appendSections([.recommendedInstances])
|
let toRemove = snapshot.itemIdentifiers(inSection: .recommendedInstances).filter {
|
||||||
snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
|
if case .recommended(_) = $0 {
|
||||||
DispatchQueue.main.async {
|
return true
|
||||||
self.dataSource.apply(snapshot)
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot.deleteItems(toRemove)
|
||||||
|
} else {
|
||||||
|
snapshot.appendSections([.recommendedInstances])
|
||||||
}
|
}
|
||||||
|
snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
|
||||||
|
self.dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
|
@ -194,29 +283,30 @@ extension InstanceSelectorTableViewController {
|
||||||
case recommended(InstanceSelector.Instance)
|
case recommended(InstanceSelector.Instance)
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
if case let .selected(url, instance) = lhs,
|
switch (lhs, rhs) {
|
||||||
case let .selected(otherUrl, other) = rhs {
|
case let (.selected(urlA, instanceA), .selected(urlB, instanceB)):
|
||||||
return url == otherUrl && instance.uri == other.uri
|
return urlA == urlB && instanceA.uri == instanceB.uri
|
||||||
} else if case let .recommended(instance) = lhs,
|
case let (.recommended(a), .recommended(b)):
|
||||||
case let .recommended(other) = rhs {
|
return a.domain == b.domain
|
||||||
return instance.domain == other.domain
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .selected(url, instance):
|
case let .selected(url, instance):
|
||||||
hasher.combine(Section.selected)
|
hasher.combine(0)
|
||||||
hasher.combine(url)
|
hasher.combine(url)
|
||||||
hasher.combine(instance.uri)
|
hasher.combine(instance.uri)
|
||||||
case let .recommended(instance):
|
case let .recommended(instance):
|
||||||
hasher.combine(Section.recommendedInstances)
|
hasher.combine(1)
|
||||||
hasher.combine(instance.domain)
|
hasher.combine(instance.domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,9 @@ struct WellnessPrefsView: View {
|
||||||
showFavAndReblogCount
|
showFavAndReblogCount
|
||||||
notificationsMode
|
notificationsMode
|
||||||
grayscaleImages
|
grayscaleImages
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
disableInfiniteScrolling
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(InsetGroupedListStyle())
|
||||||
.navigationBarTitle(Text("Digital Wellness"))
|
.navigationBarTitle(Text("Digital Wellness"))
|
||||||
|
@ -46,6 +49,14 @@ struct WellnessPrefsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var disableInfiniteScrolling: some View {
|
||||||
|
Section(footer: Text("Require a button tap before loading more posts.")) {
|
||||||
|
Toggle(isOn: $preferences.disableInfiniteScrolling) {
|
||||||
|
Text("Disable Infinite Scrolling")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WellnessPrefsView_Previews: PreviewProvider {
|
struct WellnessPrefsView_Previews: PreviewProvider {
|
||||||
|
|
|
@ -70,10 +70,11 @@ class ProfileViewController: UIPageViewController {
|
||||||
|
|
||||||
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
||||||
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
||||||
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] (_) in
|
||||||
self.composeDirectMentioning()
|
self?.composeDirectMentioning()
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
composeButton.isEnabled = mastodonController.loggedIn
|
||||||
navigationItem.rightBarButtonItem = composeButton
|
navigationItem.rightBarButtonItem = composeButton
|
||||||
|
|
||||||
headerView = ProfileHeaderView.create()
|
headerView = ProfileHeaderView.create()
|
||||||
|
@ -91,11 +92,18 @@ class ProfileViewController: UIPageViewController {
|
||||||
addKeyCommand(MenuController.nextSubTabCommand)
|
addKeyCommand(MenuController.nextSubTabCommand)
|
||||||
|
|
||||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [weak self] in $0 == self?.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [weak self] in $0 == self?.accountID }
|
||||||
.sink { [weak self] (_) in self?.updateAccountUI() }
|
.sink { [weak self] (_) in self?.updateAccountUI() }
|
||||||
|
|
||||||
loadAccount()
|
loadAccount()
|
||||||
|
|
||||||
|
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
|
||||||
|
if let nav = navigationController {
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithDefaultBackground()
|
||||||
|
nav.navigationBar.scrollEdgeAppearance = appearance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
typealias TimelineEntry = (id: String, state: StatusState)
|
typealias TimelineEntry = (id: String, state: StatusState)
|
||||||
|
|
||||||
class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry> {
|
class TimelineTableViewController: DiffableTimelineLikeTableViewController<TimelineTableViewController.Section, TimelineTableViewController.Item> {
|
||||||
|
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
@ -19,6 +19,9 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
private var newer: RequestRange?
|
private var newer: RequestRange?
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
|
private var didConfirmLoadMore = false
|
||||||
|
private var isShowingTimelineDescription = false
|
||||||
|
|
||||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
@ -40,14 +43,14 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
guard let persistentContainer = mastodonController?.persistentContainer else { return }
|
guard let persistentContainer = mastodonController?.persistentContainer,
|
||||||
|
let dataSource = dataSource else { return }
|
||||||
// decrement reference counts of any statuses we still have
|
// decrement reference counts of any statuses we still have
|
||||||
// if the app is currently being quit, this will not affect the persisted data because
|
// if the app is currently being quit, this will not affect the persisted data because
|
||||||
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
|
// the persistent container would already have been saved in SceneDelegate.sceneDidEnterBackground(_:)
|
||||||
for section in sections {
|
// todo: remove the whole reference count system
|
||||||
for (id, _) in section {
|
for case let .status(id: id, state: _) in dataSource.snapshot().itemIdentifiers {
|
||||||
persistentContainer.status(for: id)?.decrementReferenceCount()
|
persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,98 +58,249 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
||||||
|
tableView.register(UINib(nibName: "ConfirmLoadMoreTableViewCell", bundle: .main), forCellReuseIdentifier: "confirmLoadMoreCell")
|
||||||
|
tableView.register(UINib(nibName: "PublicTimelineDescriptionTableViewCell", bundle: .main), forCellReuseIdentifier: "publicTimelineDescriptionCell")
|
||||||
|
|
||||||
|
if case let .public(local: local) = timeline,
|
||||||
|
(local && !Preferences.shared.hasShownLocalTimelineDescription) || (!local && !Preferences.shared.hasShownFederatedTimelineDescription) {
|
||||||
|
isShowingTimelineDescription = true
|
||||||
|
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.appendSections([.header])
|
||||||
|
snapshot.appendItems([.publicTimelineDescription(local: local)], toSection: .header)
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if case let .public(local: local) = timeline {
|
||||||
|
if local {
|
||||||
|
Preferences.shared.hasShownLocalTimelineDescription = true
|
||||||
|
} else {
|
||||||
|
Preferences.shared.hasShownFederatedTimelineDescription = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
if isShowingTimelineDescription {
|
||||||
|
isShowingTimelineDescription = false
|
||||||
|
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.header])
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - DiffableTimelineLikeTableViewController
|
||||||
|
|
||||||
override class func refreshCommandTitle() -> String {
|
override class func refreshCommandTitle() -> String {
|
||||||
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
|
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func willRemoveRows(at indexPaths: [IndexPath]) {
|
override func timelineContentSections() -> [Section] {
|
||||||
for indexPath in indexPaths {
|
return [.statuses]
|
||||||
let id = item(for: indexPath).id
|
}
|
||||||
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
|
||||||
|
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
|
||||||
|
switch item {
|
||||||
|
case let .status(id: id, state: state):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
||||||
|
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
return cell
|
||||||
|
|
||||||
|
case .confirmLoadMore:
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "confirmLoadMoreCell", for: indexPath) as! ConfirmLoadMoreTableViewCell
|
||||||
|
cell.confirmLoadMore = {
|
||||||
|
self.didConfirmLoadMore = true
|
||||||
|
self.loadOlder()
|
||||||
|
self.didConfirmLoadMore = false
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
|
||||||
|
case .publicTimelineDescription(local: let local):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "publicTimelineDescriptionCell", for: indexPath) as! PublicTimelineDescriptionTableViewCell
|
||||||
|
cell.mastodonController = mastodonController
|
||||||
|
cell.local = local
|
||||||
|
cell.didDismiss = { [unowned self] in
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.header])
|
||||||
|
self.dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) {
|
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
|
guard let mastodonController = mastodonController else {
|
||||||
|
completion(.failure(.noClient))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline)
|
let request = Client.getStatuses(timeline: timeline)
|
||||||
|
|
||||||
mastodonController?.run(request) { (response) in
|
mastodonController.run(request) { response in
|
||||||
guard case let .success(statuses, pagination) = response else {
|
switch response {
|
||||||
completion([])
|
case let .failure(error):
|
||||||
return
|
completion(.failure(.client(error)))
|
||||||
}
|
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
case let .success(statuses, pagination):
|
||||||
self.older = pagination?.older
|
self.newer = pagination?.newer
|
||||||
|
self.older = pagination?.older
|
||||||
|
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
DispatchQueue.main.async {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.statuses, .footer])
|
||||||
|
snapshot.appendSections([.statuses, .footer])
|
||||||
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) {
|
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard let older = older else {
|
guard let older = older else {
|
||||||
completion([])
|
completion(.failure(.noOlder))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *),
|
||||||
|
Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
|
||||||
|
var snapshot = currentSnapshot()
|
||||||
|
guard !snapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
|
||||||
|
// todo: need something more accurate than "success"/"failure"
|
||||||
|
completion(.success(snapshot))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||||
|
self.dataSource.apply(snapshot)
|
||||||
|
completion(.success(snapshot))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||||
|
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { response in
|
||||||
guard case let .success(statuses, pagination) = response else {
|
switch response {
|
||||||
completion([])
|
case let .failure(error):
|
||||||
return
|
completion(.failure(.client(error)))
|
||||||
}
|
|
||||||
|
|
||||||
self.older = pagination?.older
|
case let .success(statuses, pagination):
|
||||||
|
self.older = pagination?.older
|
||||||
|
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
var snapshot = currentSnapshot()
|
||||||
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
|
snapshot.deleteItems([.confirmLoadMore])
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) {
|
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard let newer = newer else {
|
guard let newer = newer else {
|
||||||
completion([])
|
completion(.failure(.noNewer))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = Client.getStatuses(timeline: timeline, range: newer)
|
let request = Client.getStatuses(timeline: timeline, range: newer)
|
||||||
|
mastodonController.run(request) { response in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
mastodonController.run(request) { (response) in
|
case let .success(statuses, pagination):
|
||||||
guard case let .success(statuses, pagination) = response else {
|
guard !statuses.isEmpty else {
|
||||||
completion([])
|
completion(.failure(.allCaughtUp))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there are no new statuses, pagination is nil
|
// if there are no new statuses, pagination is nil
|
||||||
// if we were to then overwrite self.newer, future refreshes would fail
|
// if we were to then overwrite self.newer, future refresh would fail
|
||||||
if let newer = pagination?.newer {
|
if let newer = pagination?.newer {
|
||||||
self.newer = newer
|
self.newer = newer
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mastodonController?.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
var snapshot = currentSnapshot()
|
||||||
|
let newIdentifiers = statuses.map { Item.status(id: $0.id, state: .unknown) }
|
||||||
|
if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
|
||||||
|
snapshot.insertItems(newIdentifiers, beforeItem: first)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems(newIdentifiers, toSection: .statuses)
|
||||||
|
}
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
override func willRemoveItems(_ items: [Item]) {
|
||||||
|
for case let .status(id: id, state: _) in items {
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
mastodonController.persistentContainer.status(for: id)?.decrementReferenceCount()
|
||||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
}
|
||||||
|
|
||||||
let (id, state) = item(for: indexPath)
|
|
||||||
cell.delegate = self
|
|
||||||
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
|
|
||||||
return cell
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
super.scrollViewWillBeginDragging(scrollView)
|
||||||
|
|
||||||
|
if isShowingTimelineDescription {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.header])
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineTableViewController {
|
||||||
|
enum Section: Hashable, CaseIterable {
|
||||||
|
case header
|
||||||
|
case statuses
|
||||||
|
case footer
|
||||||
|
}
|
||||||
|
enum Item: Hashable {
|
||||||
|
case status(id: String, state: StatusState)
|
||||||
|
case confirmLoadMore
|
||||||
|
case publicTimelineDescription(local: Bool)
|
||||||
|
|
||||||
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||||
|
return a == b
|
||||||
|
case (.confirmLoadMore, .confirmLoadMore):
|
||||||
|
return true
|
||||||
|
case let (.publicTimelineDescription(local: a), .publicTimelineDescription(local: b)):
|
||||||
|
return a == b
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case let .status(id: id, state: _):
|
||||||
|
hasher.combine(0)
|
||||||
|
hasher.combine(id)
|
||||||
|
case .confirmLoadMore:
|
||||||
|
hasher.combine(1)
|
||||||
|
case let .publicTimelineDescription(local: local):
|
||||||
|
hasher.combine(2)
|
||||||
|
hasher.combine(local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController: TuskerNavigationDelegate {
|
extension TimelineTableViewController: TuskerNavigationDelegate {
|
||||||
|
@ -161,17 +315,23 @@ extension TimelineTableViewController: StatusTableViewCellDelegate {
|
||||||
|
|
||||||
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
let ids = indexPaths.map { item(for: $0).id }
|
let ids: [String] = indexPaths.compactMap {
|
||||||
|
if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
|
||||||
|
return id
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
prefetchStatuses(with: ids)
|
prefetchStatuses(with: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
let ids: [String] = indexPaths.compactMap {
|
let ids: [String] = indexPaths.compactMap {
|
||||||
guard $0.section < sections.count,
|
if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
|
||||||
$0.row < sections[$0.section].count else {
|
return id
|
||||||
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return item(for: $0).id
|
|
||||||
}
|
}
|
||||||
cancelPrefetchingStatuses(with: ids)
|
cancelPrefetchingStatuses(with: ids)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,338 @@
|
||||||
|
//
|
||||||
|
// DiffableTimelineLikeTableViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 6/18/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable, Item: Hashable>: EnhancedTableViewController, RefreshableViewController {
|
||||||
|
|
||||||
|
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>
|
||||||
|
typealias LoadResult = Result<Snapshot, LoadError>
|
||||||
|
|
||||||
|
private let pageSize = 20
|
||||||
|
|
||||||
|
private(set) var state = State.unloaded
|
||||||
|
private var lastLastVisibleRow: IndexPath?
|
||||||
|
|
||||||
|
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(style: .plain)
|
||||||
|
|
||||||
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: Self.refreshCommandTitle()))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: self.cellProvider)
|
||||||
|
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 140
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
self.refreshControl = UIRefreshControl()
|
||||||
|
self.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if let prefetchSource = self as? UITableViewDataSourcePrefetching {
|
||||||
|
tableView.prefetchDataSource = prefetchSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
loadInitial()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
pruneOffscreenRows()
|
||||||
|
currentToast?.dismissToast(animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
class func refreshCommandTitle() -> String {
|
||||||
|
return "Refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pruneOffscreenRows() {
|
||||||
|
guard let lastVisibleRow = lastLastVisibleRow else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
|
||||||
|
let lastVisibleRowSection = snapshot.sectionIdentifiers[lastVisibleRow.section]
|
||||||
|
|
||||||
|
let contentSections = snapshot.sectionIdentifiers.filter { timelineContentSections().contains($0) }
|
||||||
|
|
||||||
|
guard let lastVisibleContentSectionIndex = contentSections.lastIndex(of: lastVisibleRowSection) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastVisibleContentSectionIndex < contentSections.count - 1 {
|
||||||
|
// there are more content sections below the current last visible one
|
||||||
|
|
||||||
|
let sectionsToRemove = contentSections[lastVisibleContentSectionIndex...]
|
||||||
|
snapshot.deleteSections(Array(sectionsToRemove))
|
||||||
|
|
||||||
|
willRemoveItems(sectionsToRemove.flatMap(snapshot.itemIdentifiers(inSection:)))
|
||||||
|
} else if lastVisibleContentSectionIndex == contentSections.count - 1 {
|
||||||
|
let items = snapshot.itemIdentifiers(inSection: lastVisibleRowSection)
|
||||||
|
|
||||||
|
if lastVisibleRow.row < items.count - pageSize {
|
||||||
|
let itemsToRemove = Array(items.suffix(pageSize))
|
||||||
|
snapshot.deleteItems(itemsToRemove)
|
||||||
|
|
||||||
|
willRemoveItems(itemsToRemove)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadInitial() {
|
||||||
|
guard state == .unloaded else { return }
|
||||||
|
// set loaded immediately so we don't trigger another request while the current one is running
|
||||||
|
state = .loadingInitial
|
||||||
|
|
||||||
|
loadInitialItems() { result in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case let .success(snapshot):
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
self.state = .loaded
|
||||||
|
|
||||||
|
case let .failure(.client(error)):
|
||||||
|
self.state = .unloaded
|
||||||
|
var config = ToastConfiguration(title: "Error Loading")
|
||||||
|
config.subtitle = error.localizedDescription
|
||||||
|
config.systemImageName = error.systemImageName
|
||||||
|
config.actionTitle = "Retry"
|
||||||
|
config.action = { [weak self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self?.loadInitial()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
default:
|
||||||
|
self.state = .unloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadInitial() {
|
||||||
|
state = .unloaded
|
||||||
|
loadInitial()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOlder() {
|
||||||
|
guard state != .loadingOlder else { return }
|
||||||
|
|
||||||
|
state = .loadingOlder
|
||||||
|
|
||||||
|
loadOlderItems(currentSnapshot: dataSource.snapshot) { result in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.state = .loaded
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case let .success(snapshot):
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
case let .failure(.client(error)):
|
||||||
|
var config = ToastConfiguration(title: "Error Loading Older")
|
||||||
|
config.subtitle = error.localizedDescription
|
||||||
|
config.systemImageName = error.systemImageName
|
||||||
|
config.actionTitle = "Retry"
|
||||||
|
config.action = { [weak self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self?.loadOlder()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cellHeightChanged() {
|
||||||
|
// causes the table view to recalculate the cell heights
|
||||||
|
tableView.beginUpdates()
|
||||||
|
tableView.endUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
// this assumes that indexPathsForVisibleRows is always in order
|
||||||
|
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
||||||
|
|
||||||
|
let orderedContentSections = dataSource.snapshot().sectionIdentifiers.enumerated().filter { timelineContentSections().contains($0.element) }
|
||||||
|
if let lastContentSection = orderedContentSections.last,
|
||||||
|
indexPath.section == lastContentSection.offset,
|
||||||
|
indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
|
||||||
|
|
||||||
|
loadOlder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - RefreshableViewController
|
||||||
|
|
||||||
|
func refresh() {
|
||||||
|
guard state != .loadingNewer else { return }
|
||||||
|
|
||||||
|
state = .loadingNewer
|
||||||
|
|
||||||
|
var firstItem: Item? = nil
|
||||||
|
let currentSnapshot: () -> Snapshot = {
|
||||||
|
let snapshot = self.dataSource.snapshot()
|
||||||
|
|
||||||
|
for section in self.timelineContentSections() {
|
||||||
|
if snapshot.indexOfSection(section) != nil,
|
||||||
|
let first = snapshot.itemIdentifiers(inSection: section).first {
|
||||||
|
firstItem = first
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNewerItems(currentSnapshot: currentSnapshot) { result in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.refreshControl?.endRefreshing()
|
||||||
|
self.state = .loaded
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case let .success(snapshot):
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
if let firstItem = firstItem,
|
||||||
|
let indexPath = self.dataSource.indexPath(for: firstItem) {
|
||||||
|
// maintain the current position in the list (don't scroll to top)
|
||||||
|
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .failure(.client(error)):
|
||||||
|
var config = ToastConfiguration(title: "Error Loading Newer")
|
||||||
|
config.subtitle = error.localizedDescription
|
||||||
|
config.systemImageName = error.systemImageName
|
||||||
|
config.actionTitle = "Retry"
|
||||||
|
config.action = { [weak self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self?.refresh()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
case .failure(.allCaughtUp):
|
||||||
|
var config = ToastConfiguration(title: "You're all caught up")
|
||||||
|
config.edge = .top
|
||||||
|
config.dismissAutomaticallyAfter = 2
|
||||||
|
config.action = { (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subclass Methods
|
||||||
|
|
||||||
|
func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
|
||||||
|
fatalError("cellProvider(_:_:_:) must be implemented by subclasses")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
|
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
|
fatalError("loadOlderItesm(completion:) must be implemented by subclasses")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
|
fatalError("loadNewerItems(completion:) must be implemented by subclasses")
|
||||||
|
}
|
||||||
|
|
||||||
|
func timelineContentSections() -> Section.AllCases {
|
||||||
|
return Section.allCases
|
||||||
|
}
|
||||||
|
|
||||||
|
func willRemoveItems(_ items: [Item]) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiffableTimelineLikeTableViewController {
|
||||||
|
enum State: Equatable {
|
||||||
|
case unloaded
|
||||||
|
case loadingInitial
|
||||||
|
case loaded
|
||||||
|
case loadingNewer
|
||||||
|
case loadingOlder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiffableTimelineLikeTableViewController {
|
||||||
|
enum LoadError: LocalizedError {
|
||||||
|
case noClient
|
||||||
|
case noOlder
|
||||||
|
case noNewer
|
||||||
|
case allCaughtUp
|
||||||
|
case client(Client.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiffableTimelineLikeTableViewController: BackgroundableViewController {
|
||||||
|
func sceneDidEnterBackground() {
|
||||||
|
pruneOffscreenRows()
|
||||||
|
currentToast?.dismissToast(animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiffableTimelineLikeTableViewController: ToastableViewController {
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension Client.Error {
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .networkError(_):
|
||||||
|
return "wifi.exclamationmark"
|
||||||
|
default:
|
||||||
|
return "exclamationmark.triangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,10 +37,11 @@ extension MenuPreviewProvider {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
||||||
|
|
||||||
guard mastodonController.loggedIn else {
|
guard let loggedInAccountID = mastodonController.accountInfo?.id else {
|
||||||
|
// user is logged out
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: account.url),
|
openInSafariAction(url: account.url),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -63,38 +64,45 @@ extension MenuPreviewProvider {
|
||||||
let request = Client.getRelationships(accounts: [account.id])
|
let request = Client.getRelationships(accounts: [account.id])
|
||||||
// talk about callback hell :/
|
// talk about callback hell :/
|
||||||
mastodonController.run(request) { [weak self] (response) in
|
mastodonController.run(request) { [weak self] (response) in
|
||||||
if let self = self,
|
guard let self = self,
|
||||||
case let .success(results, _) = response,
|
case let .success(results, _) = response,
|
||||||
let relationship = results.first {
|
let relationship = results.first else {
|
||||||
let following = relationship.following
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
elementHandler([])
|
||||||
elementHandler([
|
}
|
||||||
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
|
return
|
||||||
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
}
|
||||||
mastodonController.run(request) { (response) in
|
let following = relationship.following
|
||||||
switch response {
|
DispatchQueue.main.async {
|
||||||
case .failure(_):
|
let action = self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
|
||||||
fatalError()
|
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
||||||
case let .success(relationship, _):
|
mastodonController.run(request) { (response) in
|
||||||
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
switch response {
|
||||||
}
|
case .failure(_):
|
||||||
}
|
fatalError()
|
||||||
})
|
case let .success(relationship, _):
|
||||||
])
|
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
elementHandler([
|
||||||
|
action
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
let shareSection = [
|
var shareSection = [
|
||||||
openInSafariAction(url: account.url),
|
openInSafariAction(url: account.url),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
|
||||||
|
@ -104,7 +112,7 @@ extension MenuPreviewProvider {
|
||||||
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] {
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: url),
|
openInSafariAction(url: url),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -133,13 +141,14 @@ extension MenuPreviewProvider {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsForStatus(_ status: StatusMO, sourceView: UIView?) -> [UIMenuElement] {
|
func actionsForStatus(_ status: StatusMO, sourceView: UIView?, includeReply: Bool = true) -> [UIMenuElement] {
|
||||||
guard let mastodonController = mastodonController else { return [] }
|
guard let mastodonController = mastodonController else { return [] }
|
||||||
|
|
||||||
guard mastodonController.loggedIn else {
|
guard let accountID = mastodonController.accountInfo?.id else {
|
||||||
|
// user is logged out
|
||||||
return [
|
return [
|
||||||
openInSafariAction(url: status.url!),
|
openInSafariAction(url: status.url!),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||||
})
|
})
|
||||||
|
@ -150,10 +159,6 @@ extension MenuPreviewProvider {
|
||||||
let muted = status.muted
|
let muted = status.muted
|
||||||
|
|
||||||
var actionsSection = [
|
var actionsSection = [
|
||||||
createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.navigationDelegate?.compose(inReplyToID: status.id)
|
|
||||||
}),
|
|
||||||
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
|
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
|
||||||
|
@ -174,9 +179,16 @@ extension MenuPreviewProvider {
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if includeReply {
|
||||||
|
actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.navigationDelegate?.compose(inReplyToID: status.id)
|
||||||
|
}), at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
||||||
let pinned = status.pinned ?? false
|
let pinned = status.pinned ?? false
|
||||||
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin" : "Pin", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
||||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||||
|
@ -204,21 +216,13 @@ extension MenuPreviewProvider {
|
||||||
|
|
||||||
var shareSection = [
|
var shareSection = [
|
||||||
openInSafariAction(url: status.url!),
|
openInSafariAction(url: status.url!),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView)
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
#if targetEnvironment(macCatalyst)
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
||||||
shareSection.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "", handler: { (_) in
|
|
||||||
guard let id = mastodonController.accountInfo?.id else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// todo: this should try to find an existing session
|
|
||||||
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: id), options: nil, errorHandler: nil)
|
|
||||||
}))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
|
||||||
|
@ -242,6 +246,28 @@ extension MenuPreviewProvider {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
|
||||||
|
#if SDK_IOS_15
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
|
options.preferredPresentationStyle = .automatic
|
||||||
|
actions.append(UIWindowScene.ActivationAction { (_) in
|
||||||
|
return .init(userActivity: activity(), options: options, preview: nil)
|
||||||
|
})
|
||||||
|
} else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LargeImageViewController: CustomPreviewPresenting {
|
extension LargeImageViewController: CustomPreviewPresenting {
|
||||||
|
|
|
@ -44,6 +44,13 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
||||||
|
|
||||||
addKeyCommand(MenuController.prevSubTabCommand)
|
addKeyCommand(MenuController.prevSubTabCommand)
|
||||||
addKeyCommand(MenuController.nextSubTabCommand)
|
addKeyCommand(MenuController.nextSubTabCommand)
|
||||||
|
|
||||||
|
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
|
||||||
|
if let nav = navigationController {
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithDefaultBackground()
|
||||||
|
nav.navigationBar.scrollEdgeAppearance = appearance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectPage(at index: Int, animated: Bool) {
|
func selectPage(at index: Int, animated: Bool) {
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// A table view controller that manages common functionality between timeline-like UIs.
|
/// A table view controller that manages common functionality between timeline-like UIs.
|
||||||
// For example, this class handles loading new items when the user scrolls to the end,
|
/// For example, this class handles loading new items when the user scrolls to the end,
|
||||||
// refreshing, and pruning offscreen rows automatically.
|
/// refreshing, and pruning offscreen rows automatically.
|
||||||
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
|
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
|
||||||
|
|
||||||
private(set) var loaded = false
|
private(set) var loaded = false
|
||||||
|
|
|
@ -70,6 +70,7 @@ class UserActivityManager {
|
||||||
// TODO: check not currently showing compose screen
|
// TODO: check not currently showing compose screen
|
||||||
let mentioning = activity.userInfo?["mentioning"] as? String
|
let mentioning = activity.userInfo?["mentioning"] as? String
|
||||||
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
let draft = mastodonController.createDraft(mentioningAcct: mentioning)
|
||||||
|
// todo: this shouldn't use self.mastodonController, it should get the right one based on the userInfo accountID
|
||||||
let composeVC = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
let composeVC = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
||||||
present(UINavigationController(rootViewController: composeVC))
|
present(UINavigationController(rootViewController: composeVC))
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,15 +89,22 @@ extension TuskerNavigationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft) {
|
func compose(editing draft: Draft) {
|
||||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
if #available(iOS 15.0, *), UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
|
||||||
let vc = UINavigationController(rootViewController: compose)
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
vc.presentationController?.delegate = compose
|
options.preferredPresentationStyle = .prominent
|
||||||
present(vc, animated: true)
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
|
||||||
|
} else {
|
||||||
|
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||||
|
let nav = UINavigationController(rootViewController: compose)
|
||||||
|
nav.presentationController?.delegate = compose
|
||||||
|
present(nav, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
||||||
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
||||||
|
DraftsManager.shared.add(draft)
|
||||||
compose(editing: draft)
|
compose(editing: draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,7 +164,9 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
let attachmentURL = attachment.url
|
let attachmentURL = attachment.url
|
||||||
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
|
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, _) in
|
||||||
guard let self = self, let data = data else { return }
|
guard let self = self, let data = data else { return }
|
||||||
self.attachmentRequest = nil
|
DispatchQueue.main.async {
|
||||||
|
self.attachmentRequest = nil
|
||||||
|
}
|
||||||
if self.attachment.url.pathExtension == "gif" {
|
if self.attachment.url.pathExtension == "gif" {
|
||||||
self.source = .gifData(attachmentURL, data)
|
self.source = .gifData(attachmentURL, data)
|
||||||
if self.autoplayGifs {
|
if self.autoplayGifs {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// ConfirmLoadMoreTableViewCell.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 6/23/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ConfirmLoadOlderTableViewCellDelegate: AnyObject {
|
||||||
|
func confirmLoadMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfirmLoadMoreTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
var confirmLoadMore: (() -> Void)?
|
||||||
|
|
||||||
|
@IBOutlet weak var confirmButton: UIButton!
|
||||||
|
|
||||||
|
private var isLoading = false
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
var config = UIButton.Configuration.tinted()
|
||||||
|
config.title = "Load More"
|
||||||
|
config.showsActivityIndicator = false
|
||||||
|
config.imagePadding = 4
|
||||||
|
confirmButton.configuration = config
|
||||||
|
confirmButton.configurationUpdateHandler = { [unowned self] button in
|
||||||
|
button.configuration?.showsActivityIndicator = self.isLoading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
confirmButton.setNeedsUpdateConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func loadMorePressed(_ sender: Any) {
|
||||||
|
confirmLoadMore?()
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
isLoading = true
|
||||||
|
confirmButton.setNeedsUpdateConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19115.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19107.5"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="105" id="KGk-i7-Jjw" customClass="ConfirmLoadMoreTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="105"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="105"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="Rpx-45-c2n">
|
||||||
|
<rect key="frame" x="16" y="11" width="288" height="86"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Infinite scrolling is off. Do you want to keep going?" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9nv-Re-5sL">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="288" height="41"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NT9-ly-efr">
|
||||||
|
<rect key="frame" x="0.0" y="45" width="288" height="41"/>
|
||||||
|
<state key="normal" title="Button"/>
|
||||||
|
<buttonConfiguration key="configuration" style="tinted" title="Load More"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="loadMorePressed:" destination="KGk-i7-Jjw" eventType="touchUpInside" id="Pgz-MB-icB"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="Rpx-45-c2n" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="YfZ-rr-Omf"/>
|
||||||
|
<constraint firstItem="Rpx-45-c2n" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="hhi-yX-Wa4"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="Rpx-45-c2n" secondAttribute="trailing" id="jI8-St-34M"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="Rpx-45-c2n" secondAttribute="bottom" constant="8" id="mQh-0l-Eo2"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="confirmButton" destination="NT9-ly-efr" id="Lja-th-LeH"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="131.8840579710145" y="150.33482142857142"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="secondaryLabelColor">
|
||||||
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
<systemColor name="secondarySystemBackgroundColor">
|
||||||
|
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
|
@ -143,9 +143,9 @@ class ContentTextView: LinkTextView {
|
||||||
case "del":
|
case "del":
|
||||||
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
||||||
case "code":
|
case "code":
|
||||||
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange)
|
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
||||||
case "pre":
|
case "pre":
|
||||||
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange)
|
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
case "ol", "ul":
|
case "ol", "ul":
|
||||||
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
@ -157,7 +157,7 @@ class ContentTextView: LinkTextView {
|
||||||
if parentTag == "ol" {
|
if parentTag == "ol" {
|
||||||
let index = (try? node.elementSiblingIndex()) ?? 0
|
let index = (try? node.elementSiblingIndex()) ?? 0
|
||||||
// we use the monospace digit font so that the periods of all the list items line up
|
// we use the monospace digit font so that the periods of all the list items line up
|
||||||
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: self.font!.pointSize, weight: .regular)])
|
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: defaultFont.pointSize, weight: .regular)])
|
||||||
} else if parentTag == "ul" {
|
} else if parentTag == "ul" {
|
||||||
bullet = NSAttributedString(string: "\u{2022}\t")
|
bullet = NSAttributedString(string: "\u{2022}\t")
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -33,6 +33,10 @@ class StatusPollView: UIView {
|
||||||
private var animator: UIViewPropertyAnimator!
|
private var animator: UIViewPropertyAnimator!
|
||||||
private var currentSelectedOptionIndex: Int!
|
private var currentSelectedOptionIndex: Int!
|
||||||
|
|
||||||
|
var isTracking: Bool {
|
||||||
|
optionsView.isTracking
|
||||||
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
|
|
@ -80,14 +80,14 @@ class ProfileHeaderView: UIView {
|
||||||
cancellables = []
|
cancellables = []
|
||||||
|
|
||||||
mastodonController.persistentContainer.accountSubject
|
mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [weak self] in $0 == self?.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [weak self] in $0 == self?.accountID }
|
||||||
.sink { [weak self] in self?.updateUI(for: $0) }
|
.sink { [weak self] in self?.updateUI(for: $0) }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
mastodonController.persistentContainer.relationshipSubject
|
mastodonController.persistentContainer.relationshipSubject
|
||||||
.filter { [weak self] in $0 == self?.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [weak self] in $0 == self?.accountID }
|
||||||
.sink { [weak self] (_) in self?.updateRelationship() }
|
.sink { [weak self] (_) in self?.updateRelationship() }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
@ -203,11 +203,14 @@ class ProfileHeaderView: UIView {
|
||||||
let image = image,
|
let image = image,
|
||||||
self.accountID == accountID,
|
self.accountID == accountID,
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.avatarRequest = nil
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.avatarRequest = nil
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.avatarRequest = nil
|
||||||
self.avatarImageView.image = transformedImage
|
self.avatarImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,11 +220,14 @@ class ProfileHeaderView: UIView {
|
||||||
let image = image,
|
let image = image,
|
||||||
self.accountID == accountID,
|
self.accountID == accountID,
|
||||||
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
|
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.headerRequest = nil
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.headerRequest = nil
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.headerRequest = nil
|
||||||
self.headerImageView.image = transformedImage
|
self.headerImageView.image = transformedImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,8 +106,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
open func createObserversIfNecessary() {
|
open func createObserversIfNecessary() {
|
||||||
if statusUpdater == nil {
|
if statusUpdater == nil {
|
||||||
statusUpdater = mastodonController.persistentContainer.statusSubject
|
statusUpdater = mastodonController.persistentContainer.statusSubject
|
||||||
.filter { [unowned self] in $0 == self.statusID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [unowned self] in $0 == self.statusID }
|
||||||
.sink { [unowned self] in
|
.sink { [unowned self] in
|
||||||
if let mastodonController = mastodonController,
|
if let mastodonController = mastodonController,
|
||||||
let status = mastodonController.persistentContainer.status(for: $0) {
|
let status = mastodonController.persistentContainer.status(for: $0) {
|
||||||
|
@ -118,8 +118,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
|
|
||||||
if accountUpdater == nil {
|
if accountUpdater == nil {
|
||||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [unowned self] in $0 == self.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [unowned self] in $0 == self.accountID }
|
||||||
.sink { [unowned self] in
|
.sink { [unowned self] in
|
||||||
if let mastodonController = mastodonController,
|
if let mastodonController = mastodonController,
|
||||||
let account = mastodonController.persistentContainer.account(for: $0) {
|
let account = mastodonController.persistentContainer.account(for: $0) {
|
||||||
|
@ -210,7 +210,8 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton))
|
// do not include reply action here, because the cell already contains a button for it
|
||||||
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton, includeReply: false))
|
||||||
|
|
||||||
pollView.isHidden = status.poll == nil
|
pollView.isHidden = status.poll == nil
|
||||||
pollView.mastodonController = mastodonController
|
pollView.mastodonController = mastodonController
|
||||||
|
|
|
@ -59,8 +59,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
|
|
||||||
if rebloggerAccountUpdater == nil {
|
if rebloggerAccountUpdater == nil {
|
||||||
rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject
|
rebloggerAccountUpdater = mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [unowned self] in $0 == self.rebloggerID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [unowned self] in $0 == self.rebloggerID }
|
||||||
.sink { [unowned self] in
|
.sink { [unowned self] in
|
||||||
if let mastodonController = self.mastodonController,
|
if let mastodonController = self.mastodonController,
|
||||||
let reblogger = mastodonController.persistentContainer.account(for: $0) {
|
let reblogger = mastodonController.persistentContainer.account(for: $0) {
|
||||||
|
@ -326,8 +326,11 @@ extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
|
||||||
|
|
||||||
extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
||||||
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
|
||||||
|
// the poll options view is tracking while the user is dragging between options
|
||||||
|
// while that's happening, don't initiate a drag
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID),
|
guard let status = mastodonController.persistentContainer.status(for: statusID),
|
||||||
let accountID = mastodonController.accountInfo?.id else {
|
let accountID = mastodonController.accountInfo?.id,
|
||||||
|
!pollView.isTracking else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let provider = NSItemProvider(object: status.url! as NSURL)
|
let provider = NSItemProvider(object: status.url! as NSURL)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19115.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19107.4"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -38,7 +39,7 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</imageView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
|
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
|
||||||
<rect key="frame" x="58" y="0.0" width="277" height="169.5"/>
|
<rect key="frame" x="58" y="0.0" width="277" height="173.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
|
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/>
|
||||||
|
@ -132,13 +133,19 @@
|
||||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
</view>
|
</view>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="1" verticalCompressionResistancePriority="1" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oFl-rC-EEN">
|
||||||
|
<rect key="frame" x="0.0" y="173.5" width="277" height="0.0"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
||||||
<rect key="frame" x="0.0" y="54" width="50" height="22"/>
|
<rect key="frame" x="0.0" y="54" width="50" height="22"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
|
||||||
<rect key="frame" x="0.0" y="1" width="25.5" height="21.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="25.5" height="21.5"/>
|
||||||
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
<color key="tintColor" systemColor="secondaryLabelColor"/>
|
||||||
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
|
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
@ -218,7 +225,7 @@
|
||||||
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/>
|
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/>
|
||||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
|
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
|
||||||
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="trailing" secondItem="ve3-Y1-NQH" secondAttribute="trailingMargin" id="3l0-tE-Ak1"/>
|
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="trailing" secondItem="ve3-Y1-NQH" secondAttribute="trailingMargin" id="3l0-tE-Ak1"/>
|
||||||
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="top" secondItem="gIY-Wp-RSk" secondAttribute="bottom" id="4KL-a3-qyf"/>
|
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="top" secondItem="gIY-Wp-RSk" secondAttribute="bottom" constant="-4" id="4KL-a3-qyf"/>
|
||||||
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
|
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
|
||||||
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="oie-wK-IpU" secondAttribute="bottom" id="7Xp-Sa-Rfk"/>
|
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="oie-wK-IpU" secondAttribute="bottom" id="7Xp-Sa-Rfk"/>
|
||||||
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
|
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
|
||||||
|
@ -276,7 +283,7 @@
|
||||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||||
<image name="globe" catalog="system" width="128" height="121"/>
|
<image name="globe" catalog="system" width="128" height="121"/>
|
||||||
<image name="pin.fill" catalog="system" width="119" height="128"/>
|
<image name="pin.fill" catalog="system" width="119" height="128"/>
|
||||||
<image name="repeat" catalog="system" width="128" height="99"/>
|
<image name="repeat" catalog="system" width="128" height="98"/>
|
||||||
<image name="star.fill" catalog="system" width="128" height="116"/>
|
<image name="star.fill" catalog="system" width="128" height="116"/>
|
||||||
<systemColor name="labelColor">
|
<systemColor name="labelColor">
|
||||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
//
|
||||||
|
// PublicTimelineDescriptionTableViewCell.swift
|
||||||
|
// PublicTimelineDescriptionTableViewCell
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/7/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class PublicTimelineDescriptionTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
var local = false {
|
||||||
|
didSet {
|
||||||
|
updateLabel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var didDismiss: (() -> Void)?
|
||||||
|
|
||||||
|
@IBOutlet private weak var descriptionLabel: UILabel!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
contentView.backgroundColor = .tintColor
|
||||||
|
} else {
|
||||||
|
contentView.backgroundColor = .systemBlue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLabel() {
|
||||||
|
let str = NSMutableAttributedString()
|
||||||
|
let instanceStr = NSAttributedString(string: mastodonController.instanceURL.host!, attributes: [
|
||||||
|
.font: UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0)
|
||||||
|
])
|
||||||
|
if local {
|
||||||
|
str.append(NSAttributedString(string: "The local timeline shows public posts from only "))
|
||||||
|
str.append(instanceStr)
|
||||||
|
str.append(NSAttributedString(string: "."))
|
||||||
|
} else {
|
||||||
|
str.append(NSAttributedString(string: "The federated timeline shows public posts from all users that "))
|
||||||
|
str.append(instanceStr)
|
||||||
|
str.append(NSAttributedString(string: " knows about."))
|
||||||
|
}
|
||||||
|
descriptionLabel.attributedText = str
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PublicTimelineDescriptionTableViewCell: SelectableTableViewCell {
|
||||||
|
func didSelectCell() {
|
||||||
|
didDismiss?()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19150" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19134"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="78" id="KGk-i7-Jjw" customClass="PublicTimelineDescriptionTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="78"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="78"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="N97-CH-58I">
|
||||||
|
<rect key="frame" x="16" y="8" width="288" height="62"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" systemColor="systemBlueColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="N97-CH-58I" secondAttribute="bottom" constant="8" id="2Lg-we-j2c"/>
|
||||||
|
<constraint firstItem="N97-CH-58I" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="FdS-q9-obT"/>
|
||||||
|
<constraint firstItem="N97-CH-58I" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailingMargin" id="KqX-Qy-18G"/>
|
||||||
|
<constraint firstItem="N97-CH-58I" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="Pqy-8N-OnX"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="descriptionLabel" destination="N97-CH-58I" id="z1W-HD-xy9"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="131.8840579710145" y="121.875"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="systemBlueColor">
|
||||||
|
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// ToastConfiguration.swift
|
||||||
|
// ToastConfiguration
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/14/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ToastConfiguration {
|
||||||
|
var systemImageName: String?
|
||||||
|
var titleFont: UIFont = .boldSystemFont(ofSize: 14)
|
||||||
|
var title: String
|
||||||
|
var subtitle: String?
|
||||||
|
var actionTitle: String?
|
||||||
|
var action: ((ToastView) -> Void)?
|
||||||
|
var edgeSpacing: CGFloat = 8
|
||||||
|
var edge: Edge = .automatic
|
||||||
|
var dismissOnScroll = true
|
||||||
|
var dismissAutomaticallyAfter: TimeInterval? = nil
|
||||||
|
|
||||||
|
init(title: String) {
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Edge: Equatable {
|
||||||
|
case top
|
||||||
|
case bottom
|
||||||
|
/// Determines edge based on the current device. Bottom on iPhone, top on iPad/Mac.
|
||||||
|
case automatic
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,255 @@
|
||||||
|
//
|
||||||
|
// ToastView.swift
|
||||||
|
// ToastView
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/14/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ToastView: UIView {
|
||||||
|
|
||||||
|
let configuration: ToastConfiguration
|
||||||
|
|
||||||
|
private var shrinkAnimator: UIViewPropertyAnimator?
|
||||||
|
private var recognizedGesture = false
|
||||||
|
private var shouldDismissOnScroll = false
|
||||||
|
private(set) var shouldDismissAutomatically = true
|
||||||
|
|
||||||
|
private var offscreenTranslation: CGFloat {
|
||||||
|
var translation = bounds.height + configuration.edgeSpacing
|
||||||
|
if configuration.edge == .bottom {
|
||||||
|
translation += superview?.safeAreaInsets.bottom ?? 0
|
||||||
|
} else {
|
||||||
|
translation += superview?.safeAreaInsets.top ?? 0
|
||||||
|
translation *= -1
|
||||||
|
}
|
||||||
|
return translation
|
||||||
|
}
|
||||||
|
|
||||||
|
init(configuration: ToastConfiguration) {
|
||||||
|
precondition(configuration.edge != .automatic)
|
||||||
|
self.configuration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupView() {
|
||||||
|
backgroundColor = .systemBlue
|
||||||
|
layer.shadowColor = UIColor.black.cgColor
|
||||||
|
layer.shadowRadius = 5
|
||||||
|
layer.shadowOffset = CGSize(width: 0, height: 2.5)
|
||||||
|
layer.shadowOpacity = 0.5
|
||||||
|
layer.masksToBounds = false
|
||||||
|
|
||||||
|
let stack = UIStackView()
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.spacing = 8
|
||||||
|
|
||||||
|
if let name = configuration.systemImageName {
|
||||||
|
let imageView = UIImageView(image: UIImage(systemName: name))
|
||||||
|
imageView.tintColor = .white
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
stack.addArrangedSubview(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = configuration.title
|
||||||
|
titleLabel.textColor = .white
|
||||||
|
titleLabel.font = configuration.titleFont
|
||||||
|
titleLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
|
||||||
|
if let subtitle = configuration.subtitle {
|
||||||
|
let subtitleLabel = UILabel()
|
||||||
|
subtitleLabel.text = subtitle
|
||||||
|
subtitleLabel.textColor = .white
|
||||||
|
subtitleLabel.numberOfLines = 0
|
||||||
|
subtitleLabel.font = .systemFont(ofSize: 14)
|
||||||
|
let vStack = UIStackView(arrangedSubviews: [
|
||||||
|
titleLabel,
|
||||||
|
subtitleLabel
|
||||||
|
])
|
||||||
|
vStack.axis = .vertical
|
||||||
|
vStack.spacing = 4
|
||||||
|
stack.addArrangedSubview(vStack)
|
||||||
|
} else {
|
||||||
|
stack.addArrangedSubview(titleLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let actionTitle = configuration.actionTitle {
|
||||||
|
let actionLabel = UILabel()
|
||||||
|
actionLabel.text = actionTitle
|
||||||
|
actionLabel.font = .boldSystemFont(ofSize: 16)
|
||||||
|
actionLabel.textColor = .white
|
||||||
|
stack.addArrangedSubview(actionLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubview(stack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
|
||||||
|
trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
|
||||||
|
stack.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
||||||
|
])
|
||||||
|
|
||||||
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
|
||||||
|
addGestureRecognizer(pan)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
layer.cornerRadius = min(32, bounds.height / 2)
|
||||||
|
layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: layer.cornerRadius, cornerHeight: layer.cornerRadius, transform: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissToast(animated: Bool) {
|
||||||
|
guard animated else {
|
||||||
|
removeFromSuperview()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
|
||||||
|
} completion: { (_) in
|
||||||
|
self.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateAppearance() {
|
||||||
|
self.transform = CGAffineTransform(translationX: 0, y: offscreenTranslation)
|
||||||
|
let duration = 0.5
|
||||||
|
let velocity = 0.5
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: []) {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDismissOnScroll(connectedTo scrollView: UIScrollView) {
|
||||||
|
guard configuration.dismissOnScroll else { return }
|
||||||
|
scrollView.panGestureRecognizer.addTarget(self, action: #selector(scrollViewPanGestureRecognized))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
|
recognizedGesture = false
|
||||||
|
shouldDismissAutomatically = false
|
||||||
|
|
||||||
|
shrinkAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut) {
|
||||||
|
self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
||||||
|
}
|
||||||
|
shrinkAnimator?.startAnimation(afterDelay: 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
super.touchesEnded(touches, with: event)
|
||||||
|
|
||||||
|
if !recognizedGesture {
|
||||||
|
guard let shrinkAnimator = shrinkAnimator else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shrinkAnimator.stopAnimation(true)
|
||||||
|
UIView.animate(withDuration: 0.1, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
configuration.action?(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
shrinkAnimator = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
let translation = recognizer.translation(in: self).y
|
||||||
|
|
||||||
|
let isDraggingAwayFromDismissalEdge = (configuration.edge == .top && translation > 0) || (configuration.edge == .bottom && translation < 0)
|
||||||
|
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
recognizedGesture = true
|
||||||
|
shouldDismissAutomatically = false
|
||||||
|
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
var distance: CGFloat
|
||||||
|
if isDraggingAwayFromDismissalEdge {
|
||||||
|
distance = sqrt(abs(translation))
|
||||||
|
if configuration.edge == .bottom {
|
||||||
|
distance *= -1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
distance = translation
|
||||||
|
}
|
||||||
|
transform = CGAffineTransform(translationX: 0, y: distance)
|
||||||
|
|
||||||
|
case .ended, .cancelled:
|
||||||
|
let velocity = recognizer.velocity(in: self).y
|
||||||
|
let distance = isDraggingAwayFromDismissalEdge ? sqrt(abs(translation)) : translation
|
||||||
|
|
||||||
|
let minDismissalDistance = configuration.edgeSpacing + bounds.height / 2
|
||||||
|
let dismissDueToDistance = configuration.edge == .bottom ? distance > minDismissalDistance : -distance > minDismissalDistance
|
||||||
|
|
||||||
|
let minDismissalVelocity: CGFloat = 250
|
||||||
|
let dismissDueToVelocity = configuration.edge == .bottom ? velocity > minDismissalDistance : velocity < -minDismissalVelocity
|
||||||
|
if dismissDueToDistance || dismissDueToVelocity {
|
||||||
|
|
||||||
|
if abs(translation) < abs(offscreenTranslation) {
|
||||||
|
let distanceLeft = offscreenTranslation - translation
|
||||||
|
let duration = 1 / TimeInterval(max(velocity, minDismissalVelocity) / distanceLeft)
|
||||||
|
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, options: .allowUserInteraction) {
|
||||||
|
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
|
||||||
|
} completion: { (_) in
|
||||||
|
self.removeFromSuperview()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let duration = 0.5
|
||||||
|
let velocity = distance * duration
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: .allowUserInteraction) {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func scrollViewPanGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
shouldDismissOnScroll = true
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
let translation = recognizer.translation(in: recognizer.view).y
|
||||||
|
if shouldDismissOnScroll && abs(translation) > 50 {
|
||||||
|
dismissToast(animated: true)
|
||||||
|
shouldDismissOnScroll = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case .ended, .cancelled:
|
||||||
|
shouldDismissOnScroll = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
//
|
||||||
|
// ToastableViewController.swift
|
||||||
|
// ToastableViewController
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/14/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ToastableViewController: UIViewController {
|
||||||
|
|
||||||
|
var toastParentView: UIView { get }
|
||||||
|
var toastScrollView: UIScrollView? { get }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentToastKey = "Tusker_currentToast"
|
||||||
|
|
||||||
|
extension ToastableViewController {
|
||||||
|
|
||||||
|
private(set) var currentToast: ToastView? {
|
||||||
|
get {
|
||||||
|
let holder = objc_getAssociatedObject(self, ¤tToastKey) as? WeakHolder<ToastView>
|
||||||
|
return holder?.object
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let newValue = newValue {
|
||||||
|
objc_setAssociatedObject(self, ¤tToastKey, WeakHolder(object: newValue), .OBJC_ASSOCIATION_RETAIN)
|
||||||
|
} else {
|
||||||
|
objc_setAssociatedObject(self, ¤tToastKey, nil, .OBJC_ASSOCIATION_RETAIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toastParentView: UIView { view }
|
||||||
|
var toastScrollView: UIScrollView? { view as? UIScrollView }
|
||||||
|
|
||||||
|
func showToast(configuration config: ToastConfiguration, animated: Bool) {
|
||||||
|
currentToast?.dismissToast(animated: false)
|
||||||
|
|
||||||
|
var config = config
|
||||||
|
config.edge = effectiveEdge(edge: config.edge)
|
||||||
|
|
||||||
|
let toast = ToastView(configuration: config)
|
||||||
|
currentToast = toast
|
||||||
|
|
||||||
|
let parentSafeArea = toastParentView.safeAreaLayoutGuide
|
||||||
|
toast.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
toastParentView.addSubview(toast)
|
||||||
|
|
||||||
|
let yConstraint: NSLayoutConstraint
|
||||||
|
switch config.edge {
|
||||||
|
case .top:
|
||||||
|
yConstraint = toast.topAnchor.constraint(equalTo: parentSafeArea.topAnchor, constant: config.edgeSpacing)
|
||||||
|
case .bottom:
|
||||||
|
yConstraint = parentSafeArea.bottomAnchor.constraint(equalTo: toast.bottomAnchor, constant: config.edgeSpacing)
|
||||||
|
case .automatic:
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
toast.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: parentSafeArea.leadingAnchor, multiplier: 1),
|
||||||
|
parentSafeArea.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: toast.trailingAnchor, multiplier: 1),
|
||||||
|
parentSafeArea.centerXAnchor.constraint(equalTo: toast.centerXAnchor),
|
||||||
|
yConstraint,
|
||||||
|
])
|
||||||
|
|
||||||
|
if animated {
|
||||||
|
toast.animateAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.dismissOnScroll,
|
||||||
|
let scrollView = toastScrollView {
|
||||||
|
toast.setupDismissOnScroll(connectedTo: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let time = config.dismissAutomaticallyAfter {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + time) { [weak toast] in
|
||||||
|
guard let toast = toast, toast.shouldDismissAutomatically else { return }
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func effectiveEdge(edge: ToastConfiguration.Edge) -> ToastConfiguration.Edge {
|
||||||
|
guard case .automatic = edge else {
|
||||||
|
return edge
|
||||||
|
}
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
return .bottom
|
||||||
|
} else {
|
||||||
|
return .top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate class WeakHolder<T: AnyObject> {
|
||||||
|
weak var object: T?
|
||||||
|
|
||||||
|
init(object: T) {
|
||||||
|
self.object = object
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue