Compare commits
No commits in common. "b38c24b3474821a0de050dfa4945c39e774147b2" and "22022f5ef65fb3aad8feb09dbdaaaf3302c426e2" have entirely different histories.
b38c24b347
...
22022f5ef6
|
@ -1,4 +1,3 @@
|
||||||
Dist.xcconfig
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
MyPlayground.playground/
|
MyPlayground.playground/
|
||||||
|
|
||||||
|
|
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -1,45 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2022.1 (43)
|
|
||||||
Features/Improvements:
|
|
||||||
- Re-add undo scroll-to-top by tapping the status bar a second time
|
|
||||||
- Convert hashtag/list/instance timelines to use new timeline implementation
|
|
||||||
- Clarify warning on Post Content Type preference
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix crash when refreshing profile before it loaded
|
|
||||||
- Fix crash when tapping Load More when infinite scrolling is disabled
|
|
||||||
- Fix crash when profile screen is closed before loading finishes
|
|
||||||
- Fix having to tap Cancel twice to dismiss Find Instance screen
|
|
||||||
|
|
||||||
## 2022.1 (42)
|
|
||||||
Features/Improvements:
|
|
||||||
- Add automatic crash reporting
|
|
||||||
- Tweak spacing on timeline statuses
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix status collapse/expand not animating on profiles
|
|
||||||
- Fix crash when opening profile for unloaded account (e.g., by tapping mentions)
|
|
||||||
- macOS: Add workaround for Follow/Unfollow menu item never loading
|
|
||||||
|
|
||||||
## 2022.1 (41)
|
|
||||||
Features/Improvements:
|
|
||||||
- Rewrite profile screens to use new timeline implementation
|
|
||||||
- Disable Infinite Scrolling preference (Preferences -> Digital Wellness) now applies to profiles
|
|
||||||
- Improve behavior when switching tabs on profiles
|
|
||||||
- Improve pointer interaction on timeline status cells
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Fix crash when loading images in certain circumstances
|
|
||||||
- Fix gallery dismissal leaving status bar hidden and breaking future gallery dismisses
|
|
||||||
- Fix timeline scroll position changing after dismissing gallery
|
|
||||||
- Fix images flickering when switching back to the Home tab
|
|
||||||
- Fix crash reporter being dismissed when sending email is cancelled
|
|
||||||
- Fix crash when long pressing Send Report button in crash reporter on iPad
|
|
||||||
- Fix Live Text controls not hiding when other gallery controls are hidden
|
|
||||||
- Fix replies appearing multiple times in Drafts list
|
|
||||||
- Fix crash when displaying blur hash images on Pleroma
|
|
||||||
|
|
||||||
## 2022.1 (40)
|
## 2022.1 (40)
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
- Fix selecting reblogged statuses in the timeline
|
- Fix selecting reblogged statuses in the timeline
|
||||||
|
|
|
@ -12,7 +12,6 @@ public class Instance: Decodable {
|
||||||
public let uri: String
|
public let uri: String
|
||||||
public let title: String
|
public let title: String
|
||||||
public let description: String
|
public let description: String
|
||||||
public let shortDescription: String?
|
|
||||||
public let email: String?
|
public let email: String?
|
||||||
public let version: String
|
public let version: String
|
||||||
public let urls: [String: URL]
|
public let urls: [String: URL]
|
||||||
|
@ -41,7 +40,6 @@ public class Instance: Decodable {
|
||||||
self.uri = try container.decode(String.self, forKey: .uri)
|
self.uri = try container.decode(String.self, forKey: .uri)
|
||||||
self.title = try container.decode(String.self, forKey: .title)
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
self.description = try container.decode(String.self, forKey: .description)
|
self.description = try container.decode(String.self, forKey: .description)
|
||||||
self.shortDescription = try container.decodeIfPresent(String.self, forKey: .shortDescription)
|
|
||||||
self.email = try container.decodeIfPresent(String.self, forKey: .email)
|
self.email = try container.decodeIfPresent(String.self, forKey: .email)
|
||||||
self.version = try container.decode(String.self, forKey: .version)
|
self.version = try container.decode(String.self, forKey: .version)
|
||||||
if let urls = try? container.decodeIfPresent([String: URL].self, forKey: .urls) {
|
if let urls = try? container.decodeIfPresent([String: URL].self, forKey: .urls) {
|
||||||
|
@ -74,7 +72,6 @@ public class Instance: Decodable {
|
||||||
case uri
|
case uri
|
||||||
case title
|
case title
|
||||||
case description
|
case description
|
||||||
case shortDescription = "short_description"
|
|
||||||
case email
|
case email
|
||||||
case version
|
case version
|
||||||
case urls
|
case urls
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */; };
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */; };
|
||||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
|
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
||||||
|
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */; };
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
|
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
|
||||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; };
|
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; };
|
||||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */; };
|
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */; };
|
||||||
|
D6114E0927F3EA3D0080E273 /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */; };
|
||||||
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */; };
|
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */; };
|
||||||
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
|
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
|
||||||
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */; };
|
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */; };
|
||||||
|
@ -35,15 +37,12 @@
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
||||||
D61ABEF628EE74D400B29151 /* StatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61ABEF528EE74D400B29151 /* StatusCollectionViewCell.swift */; };
|
D61ABEF628EE74D400B29151 /* StatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61ABEF528EE74D400B29151 /* StatusCollectionViewCell.swift */; };
|
||||||
D61ABEF828EFC3F900B29151 /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61ABEF728EFC3F900B29151 /* ProfileStatusesViewController.swift */; };
|
|
||||||
D61ABEFC28F105DE00B29151 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D61ABEFB28F105DE00B29151 /* Pachyderm */; };
|
D61ABEFC28F105DE00B29151 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D61ABEFB28F105DE00B29151 /* Pachyderm */; };
|
||||||
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61ABEFD28F1C92600B29151 /* FavoriteService.swift */; };
|
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61ABEFD28F1C92600B29151 /* FavoriteService.swift */; };
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||||
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */; };
|
||||||
D61DC84628F498F200B82C6E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84528F498F200B82C6E /* Logging.swift */; };
|
D61DC84628F498F200B82C6E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84528F498F200B82C6E /* Logging.swift */; };
|
||||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */; };
|
|
||||||
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61DC84C28F500D200B82C6E /* ProfileViewController.swift */; };
|
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||||
|
@ -94,15 +93,13 @@
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */; };
|
||||||
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = D63CC701290EC0B8000E19DE /* Sentry */; };
|
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */; };
|
||||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */; };
|
|
||||||
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */; };
|
|
||||||
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */; };
|
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63D8DF32850FE7A008D95E1 /* ViewTags.swift */; };
|
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63D8DF32850FE7A008D95E1 /* ViewTags.swift */; };
|
||||||
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
||||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
||||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; };
|
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; };
|
||||||
|
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0424B0227D00F5412E /* ProfileViewController.swift */; };
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0824B0291E00F5412E /* MyProfileViewController.swift */; };
|
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0824B0291E00F5412E /* MyProfileViewController.swift */; };
|
||||||
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 */; };
|
||||||
|
@ -125,6 +122,7 @@
|
||||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
|
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AAC2128D88B005A6F37 /* LocalData.swift */; };
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||||
|
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
|
||||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
||||||
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 */; };
|
||||||
|
@ -197,6 +195,7 @@
|
||||||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
|
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
|
||||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
|
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
|
||||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
|
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
|
||||||
|
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = D69CCBBE249E6EFD000AF167 /* CrashReporter */; };
|
||||||
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
||||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
|
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
|
||||||
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */; };
|
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A3BC7B232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib */; };
|
||||||
|
@ -366,6 +365,7 @@
|
||||||
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingLargeImageViewController.swift; sourceTree = "<group>"; };
|
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingLargeImageViewController.swift; sourceTree = "<group>"; };
|
||||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = "<group>"; };
|
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = "<group>"; };
|
||||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
||||||
|
0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentActionPrefs.swift; sourceTree = "<group>"; };
|
||||||
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -380,6 +380,7 @@
|
||||||
D60E2F252442372B005F8713 /* AccountMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMO.swift; sourceTree = "<group>"; };
|
D60E2F252442372B005F8713 /* AccountMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMO.swift; sourceTree = "<group>"; };
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazilyDecoding.swift; sourceTree = "<group>"; };
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazilyDecoding.swift; sourceTree = "<group>"; };
|
||||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCachePersistentStore.swift; sourceTree = "<group>"; };
|
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCachePersistentStore.swift; sourceTree = "<group>"; };
|
||||||
|
D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; };
|
||||||
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusesViewController.swift; sourceTree = "<group>"; };
|
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusesViewController.swift; sourceTree = "<group>"; };
|
||||||
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; };
|
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; };
|
||||||
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = "<group>"; };
|
D6114E1227F89B440080E273 /* TrendingLinkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinkTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -390,14 +391,11 @@
|
||||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
D61ABEF528EE74D400B29151 /* StatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61ABEF528EE74D400B29151 /* StatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61ABEF728EFC3F900B29151 /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D61ABEFD28F1C92600B29151 /* FavoriteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteService.swift; sourceTree = "<group>"; };
|
D61ABEFD28F1C92600B29151 /* FavoriteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteService.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
D61AC1D7232EA42D00C54D2D /* InstanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstanceTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D61DC84528F498F200B82C6E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
|
D61DC84528F498F200B82C6E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
|
||||||
D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderCollectionViewCell.swift; sourceTree = "<group>"; };
|
|
||||||
D61DC84C28F500D200B82C6E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -448,15 +446,13 @@
|
||||||
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
||||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
|
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
|
||||||
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = "<group>"; };
|
D6370B9B24421FF30092A7FF /* Tusker.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tusker.xcdatamodel; sourceTree = "<group>"; };
|
||||||
D63CC703290EC472000E19DE /* Dist.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Dist.xcconfig; sourceTree = "<group>"; };
|
D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
|
||||||
D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerSceneDelegate.swift; sourceTree = "<group>"; };
|
|
||||||
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Top.swift"; sourceTree = "<group>"; };
|
|
||||||
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarTappableViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = "<group>"; };
|
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = "<group>"; };
|
||||||
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
||||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; };
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6412B0424B0227D00F5412E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = "<group>"; };
|
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
@ -479,6 +475,7 @@
|
||||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
|
D64D0AAC2128D88B005A6F37 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
||||||
|
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
@ -685,10 +682,10 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
||||||
|
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
||||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
||||||
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
||||||
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -880,17 +877,6 @@
|
||||||
path = CoreData;
|
path = CoreData;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D63CC70A2910AAC6000E19DE /* Scenes */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
|
|
||||||
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
|
|
||||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
|
||||||
D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */,
|
|
||||||
);
|
|
||||||
path = Scenes;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D641C780213DD7C4004B4513 /* Screens */ = {
|
D641C780213DD7C4004B4513 /* Screens */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -925,6 +911,7 @@
|
||||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */,
|
||||||
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
|
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */,
|
||||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
|
||||||
|
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */,
|
||||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
|
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
|
||||||
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */,
|
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */,
|
||||||
);
|
);
|
||||||
|
@ -956,10 +943,9 @@
|
||||||
D641C784213DD819004B4513 /* Profile */ = {
|
D641C784213DD819004B4513 /* Profile */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D6412B0424B0227D00F5412E /* ProfileViewController.swift */,
|
||||||
|
D63A8D0A2561C27F00D9DFFF /* ProfileStatusesViewController.swift */,
|
||||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */,
|
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */,
|
||||||
D61DC84C28F500D200B82C6E /* ProfileViewController.swift */,
|
|
||||||
D61ABEF728EFC3F900B29151 /* ProfileStatusesViewController.swift */,
|
|
||||||
D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */,
|
|
||||||
);
|
);
|
||||||
path = Profile;
|
path = Profile;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1033,6 +1019,7 @@
|
||||||
D68015412401A74600D6103B /* MediaPrefsView.swift */,
|
D68015412401A74600D6103B /* MediaPrefsView.swift */,
|
||||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
||||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
|
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
|
||||||
|
0427037B22B316B9000D31B6 /* SilentActionPrefs.swift */,
|
||||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
|
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
|
||||||
);
|
);
|
||||||
path = Preferences;
|
path = Preferences;
|
||||||
|
@ -1154,7 +1141,6 @@
|
||||||
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
|
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
|
||||||
D62E9984279CA23900C26176 /* URLSession+Development.swift */,
|
D62E9984279CA23900C26176 /* URLSession+Development.swift */,
|
||||||
D6ADB6ED28EA74E8009924AB /* UIView+Configure.swift */,
|
D6ADB6ED28EA74E8009924AB /* UIView+Configure.swift */,
|
||||||
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
|
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1332,7 +1318,6 @@
|
||||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
||||||
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */,
|
|
||||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||||
|
@ -1358,7 +1343,6 @@
|
||||||
D6D4DDC3212518A000E1C4BB = {
|
D6D4DDC3212518A000E1C4BB = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D63CC703290EC472000E19DE /* Dist.xcconfig */,
|
|
||||||
D674A50727F910F300BA03AC /* Pachyderm */,
|
D674A50727F910F300BA03AC /* Pachyderm */,
|
||||||
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
||||||
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
||||||
|
@ -1388,11 +1372,14 @@
|
||||||
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
||||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
|
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
|
||||||
|
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
|
||||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||||
D61DC84528F498F200B82C6E /* Logging.swift */,
|
D61DC84528F498F200B82C6E /* Logging.swift */,
|
||||||
|
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */,
|
D6B81F432560390300F6E31D /* MenuController.swift */,
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||||
|
@ -1410,7 +1397,6 @@
|
||||||
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
||||||
D61959D2241E846D00A37B8E /* Models */,
|
D61959D2241E846D00A37B8E /* Models */,
|
||||||
D663626021360A9600C9CBA2 /* Preferences */,
|
D663626021360A9600C9CBA2 /* Preferences */,
|
||||||
D63CC70A2910AAC6000E19DE /* Scenes */,
|
|
||||||
D641C780213DD7C4004B4513 /* Screens */,
|
D641C780213DD7C4004B4513 /* Screens */,
|
||||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
|
@ -1483,6 +1469,7 @@
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */ = {
|
D6F2E960249E772F005846BB /* Crash Reporter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */,
|
||||||
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */,
|
D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */,
|
||||||
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */,
|
D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */,
|
||||||
);
|
);
|
||||||
|
@ -1514,7 +1501,7 @@
|
||||||
D6F953E52125197500CF0F2B /* Embed Frameworks */,
|
D6F953E52125197500CF0F2B /* Embed Frameworks */,
|
||||||
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
|
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */,
|
||||||
D6E3438F2659849800C4AA01 /* Embed Foundation Extensions */,
|
D6E3438F2659849800C4AA01 /* Embed Foundation Extensions */,
|
||||||
D63CC704290EC913000E19DE /* ShellScript */,
|
D6F1F9E127B0677000CB7D88 /* ShellScript */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
@ -1523,11 +1510,11 @@
|
||||||
);
|
);
|
||||||
name = Tusker;
|
name = Tusker;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
D69CCBBE249E6EFD000AF167 /* CrashReporter */,
|
||||||
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
|
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
|
||||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
||||||
D674A50827F9128D00BA03AC /* Pachyderm */,
|
D674A50827F9128D00BA03AC /* Pachyderm */,
|
||||||
D6552366289870790048A653 /* ScreenCorners */,
|
D6552366289870790048A653 /* ScreenCorners */,
|
||||||
D63CC701290EC0B8000E19DE /* Sentry */,
|
|
||||||
);
|
);
|
||||||
productName = Tusker;
|
productName = Tusker;
|
||||||
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
||||||
|
@ -1634,10 +1621,10 @@
|
||||||
);
|
);
|
||||||
mainGroup = D6D4DDC3212518A000E1C4BB;
|
mainGroup = D6D4DDC3212518A000E1C4BB;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
|
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
|
||||||
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||||
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
||||||
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
||||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
|
||||||
);
|
);
|
||||||
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
|
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -1714,26 +1701,6 @@
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
D63CC704290EC913000E19DE /* ShellScript */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
alwaysOutOfDate = 1;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}",
|
|
||||||
);
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/bash;
|
|
||||||
shellScript = "echo \"$CONFIGURATION\"\nif [ \"$CONFIGURATION\" == \"Dist\" ]; then\nif which sentry-cli >/dev/null; then\nexport SENTRY_ORG=vaccor\nexport SENTRY_PROJECT=tusker\necho \"DWARF_DSYM_FOLDER_PATH: $DWARF_DSYM_FOLDER_PATH\"\nsentry-cli upload-dif \"$DWARF_DSYM_FOLDER_PATH\" --force-foreground 2>&1\nif [ $? -eq 0 ]; then\necho \"sentry-cli uploaded debug info\"\nelse\necho \"error: sentry-cli failed\"\nexit 1\nfi\nelse\necho \"error: sentry-cli not installed, download from https://github.com/getsentry/sentry-cli/releases\"\nexit 1\nfi\nfi\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */ = {
|
D65F612C23AE957600F3CFD3 /* Embed debug-only frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -1756,6 +1723,24 @@
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "#if [ \"${CONFIGURATION}\" == \"Debug\" ]; then\n# echo \"Embedding ${SCRIPT_INPUT_FILE_0}\"\n# cp -R $SCRIPT_INPUT_FILE_0 $SCRIPT_OUTPUT_FILE_0\n# codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_0\n# \n# echo \"Embedding ${SCRIPT_INPUT_FILE_1}\"\n# cp -R $SCRIPT_INPUT_FILE_1 $SCRIPT_OUTPUT_FILE_1\n# codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_1\n#else\n# echo \"Skipping embedding debug frameworks\"\n#fi\n";
|
shellScript = "#if [ \"${CONFIGURATION}\" == \"Debug\" ]; then\n# echo \"Embedding ${SCRIPT_INPUT_FILE_0}\"\n# cp -R $SCRIPT_INPUT_FILE_0 $SCRIPT_OUTPUT_FILE_0\n# codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_0\n# \n# echo \"Embedding ${SCRIPT_INPUT_FILE_1}\"\n# cp -R $SCRIPT_INPUT_FILE_1 $SCRIPT_OUTPUT_FILE_1\n# codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY $SCRIPT_OUTPUT_FILE_1\n#else\n# echo \"Skipping embedding debug frameworks\"\n#fi\n";
|
||||||
};
|
};
|
||||||
|
D6F1F9E127B0677000CB7D88 /* ShellScript */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\n# the nested framework doens't get signed automatically for some reason\n# so we sign it ourselves\n#codesign --force --verbose --sign $EXPANDED_CODE_SIGN_IDENTITY \"$BUILT_PRODUCTS_DIR/Tusker.app/Frameworks/Pachyderm.framework/Frameworks/WebURL.framework\"\n\n# xcode wants to include the weburl framework twice for some reason, but the app store doesn't like nested frameworks\n# we already have a copy in the app's Frameworks/ dir, so we can delete the nested one\nif [ \"$(ls \"$BUILT_PRODUCTS_DIR/Tusker.app/Frameworks/Pachyderm.framework/Frameworks/\")\" -ne \"WebURL.framework\" ]; then\n exit 1\nfi\nrm -rf \"$BUILT_PRODUCTS_DIR/Tusker.app/Frameworks/Pachyderm.framework/Frameworks/\"\n";
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
@ -1796,7 +1781,6 @@
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||||
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */,
|
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
||||||
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
||||||
|
@ -1827,7 +1811,7 @@
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
||||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
||||||
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
|
D6B9366D2828445000237D0E /* SavedInstance.swift in Sources */,
|
||||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||||
|
@ -1848,7 +1832,6 @@
|
||||||
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
|
||||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||||
|
@ -1875,7 +1858,6 @@
|
||||||
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
||||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
||||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||||
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
|
|
||||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,
|
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,
|
||||||
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */,
|
D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */,
|
||||||
|
@ -1891,7 +1873,6 @@
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */,
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
|
|
||||||
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
||||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||||
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
|
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
|
||||||
|
@ -1972,7 +1953,6 @@
|
||||||
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
|
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
|
||||||
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */,
|
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */,
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
|
||||||
D61ABEF828EFC3F900B29151 /* ProfileStatusesViewController.swift in Sources */,
|
|
||||||
D6ADB6EA28E91C30009924AB /* TimelineStatusCollectionViewCell.swift in Sources */,
|
D6ADB6EA28E91C30009924AB /* TimelineStatusCollectionViewCell.swift in Sources */,
|
||||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
||||||
|
@ -1992,10 +1972,13 @@
|
||||||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
||||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||||
|
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
|
||||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||||
|
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */,
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||||
|
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,
|
||||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||||
|
@ -2008,6 +1991,7 @@
|
||||||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
||||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
||||||
|
D6114E0927F3EA3D0080E273 /* CrashReporterViewController.swift in Sources */,
|
||||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
|
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||||
|
@ -2103,161 +2087,6 @@
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
D63CC705290ECE77000E19DE /* Dist */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
baseConfigurationReference = D63CC703290EC472000E19DE /* Dist.xcconfig */;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
|
||||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
|
||||||
VALIDATE_PRODUCT = YES;
|
|
||||||
};
|
|
||||||
name = Dist;
|
|
||||||
};
|
|
||||||
D63CC706290ECE77000E19DE /* Dist */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 43;
|
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 2022.1;
|
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
|
||||||
SUPPORTS_MACCATALYST = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
|
||||||
};
|
|
||||||
name = Dist;
|
|
||||||
};
|
|
||||||
D63CC707290ECE77000E19DE /* Dist */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
|
||||||
INFOPLIST_FILE = TuskerTests/Info.plist;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@loader_path/Frameworks",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.TuskerTests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Tusker.app/Tusker";
|
|
||||||
};
|
|
||||||
name = Dist;
|
|
||||||
};
|
|
||||||
D63CC708290ECE77000E19DE /* Dist */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@loader_path/Frameworks",
|
|
||||||
);
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.TuskerUITests;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
TEST_TARGET_NAME = Tusker;
|
|
||||||
};
|
|
||||||
name = Dist;
|
|
||||||
};
|
|
||||||
D63CC709290ECE77000E19DE /* Dist */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 43;
|
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 2022.1;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.OpenInTusker;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
SKIP_INSTALL = YES;
|
|
||||||
SUPPORTS_MACCATALYST = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
|
||||||
};
|
|
||||||
name = Dist;
|
|
||||||
};
|
|
||||||
D6D4DDF2212518A200E1C4BB /* Debug */ = {
|
D6D4DDF2212518A200E1C4BB /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
@ -2388,7 +2217,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 = 43;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2417,7 +2246,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 = 43;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2527,7 +2356,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 = 43;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
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;
|
||||||
|
@ -2554,7 +2383,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 = 43;
|
CURRENT_PROJECT_VERSION = 40;
|
||||||
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;
|
||||||
|
@ -2582,7 +2411,6 @@
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
D6D4DDF2212518A200E1C4BB /* Debug */,
|
D6D4DDF2212518A200E1C4BB /* Debug */,
|
||||||
D6D4DDF3212518A200E1C4BB /* Release */,
|
D6D4DDF3212518A200E1C4BB /* Release */,
|
||||||
D63CC705290ECE77000E19DE /* Dist */,
|
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
@ -2592,7 +2420,6 @@
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
D6D4DDF5212518A200E1C4BB /* Debug */,
|
D6D4DDF5212518A200E1C4BB /* Debug */,
|
||||||
D6D4DDF6212518A200E1C4BB /* Release */,
|
D6D4DDF6212518A200E1C4BB /* Release */,
|
||||||
D63CC706290ECE77000E19DE /* Dist */,
|
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
@ -2602,7 +2429,6 @@
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
D6D4DDF8212518A200E1C4BB /* Debug */,
|
D6D4DDF8212518A200E1C4BB /* Debug */,
|
||||||
D6D4DDF9212518A200E1C4BB /* Release */,
|
D6D4DDF9212518A200E1C4BB /* Release */,
|
||||||
D63CC707290ECE77000E19DE /* Dist */,
|
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
@ -2612,7 +2438,6 @@
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
D6D4DDFB212518A200E1C4BB /* Debug */,
|
D6D4DDFB212518A200E1C4BB /* Debug */,
|
||||||
D6D4DDFC212518A200E1C4BB /* Release */,
|
D6D4DDFC212518A200E1C4BB /* Release */,
|
||||||
D63CC708290ECE77000E19DE /* Dist */,
|
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
@ -2622,7 +2447,6 @@
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
D6E343B7265AAD6B00C4AA01 /* Debug */,
|
D6E343B7265AAD6B00C4AA01 /* Debug */,
|
||||||
D6E343B8265AAD6B00C4AA01 /* Release */,
|
D6E343B8265AAD6B00C4AA01 /* Release */,
|
||||||
D63CC709290ECE77000E19DE /* Dist */,
|
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
@ -2638,14 +2462,6 @@
|
||||||
minimumVersion = 2.3.2;
|
minimumVersion = 2.3.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMinorVersion;
|
|
||||||
minimumVersion = 7.29.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = {
|
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/kylebshr/ScreenCorners";
|
repositoryURL = "https://github.com/kylebshr/ScreenCorners";
|
||||||
|
@ -2662,6 +2478,14 @@
|
||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/microsoft/plcrashreporter";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMinorVersion;
|
||||||
|
minimumVersion = 1.8.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -2674,11 +2498,6 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Pachyderm;
|
productName = Pachyderm;
|
||||||
};
|
};
|
||||||
D63CC701290EC0B8000E19DE /* Sentry */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */;
|
|
||||||
productName = Sentry;
|
|
||||||
};
|
|
||||||
D6552366289870790048A653 /* ScreenCorners */ = {
|
D6552366289870790048A653 /* ScreenCorners */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */;
|
package = D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */;
|
||||||
|
@ -2693,6 +2512,11 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Pachyderm;
|
productName = Pachyderm;
|
||||||
};
|
};
|
||||||
|
D69CCBBE249E6EFD000AF167 /* CrashReporter */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
|
||||||
|
productName = CrashReporter;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
/* Begin XCVersionGroup section */
|
/* Begin XCVersionGroup section */
|
||||||
|
|
|
@ -104,6 +104,11 @@
|
||||||
value = ""
|
value = ""
|
||||||
isEnabled = "NO">
|
isEnabled = "NO">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "DEBUG_BLUR_HASH"
|
||||||
|
value = "1"
|
||||||
|
isEnabled = "NO">
|
||||||
|
</EnvironmentVariable>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
|
@ -127,7 +132,7 @@
|
||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
</AnalyzeAction>
|
</AnalyzeAction>
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
buildConfiguration = "Dist"
|
buildConfiguration = "Release"
|
||||||
revealArchiveInOrganizer = "YES">
|
revealArchiveInOrganizer = "YES">
|
||||||
</ArchiveAction>
|
</ArchiveAction>
|
||||||
</Scheme>
|
</Scheme>
|
||||||
|
|
|
@ -50,10 +50,6 @@ struct InstanceFeatures {
|
||||||
|| (instanceType == .pleroma && hasVersion(2, 0, 0))
|
|| (instanceType == .pleroma && hasVersion(2, 0, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
var probablySupportsMarkdown: Bool {
|
|
||||||
instanceType == .pleroma || instanceType == .glitch || instanceType == .hometown
|
|
||||||
}
|
|
||||||
|
|
||||||
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
mutating func update(instance: Instance, nodeInfo: NodeInfo?) {
|
||||||
let ver = instance.version.lowercased()
|
let ver = instance.version.lowercased()
|
||||||
if ver.contains("glitch") {
|
if ver.contains("glitch") {
|
||||||
|
|
|
@ -68,7 +68,7 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
func run<Result>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||||
let result: (Result, Pagination?) = try await withCheckedThrowingContinuation({ continuation in
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
client.run(request) { response in
|
client.run(request) { response in
|
||||||
switch response {
|
switch response {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
|
@ -78,8 +78,6 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
try Task.checkCancellation()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - Returns: A tuple of client ID and client secret.
|
/// - Returns: A tuple of client ID and client secret.
|
||||||
|
|
|
@ -7,19 +7,22 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import CrashReporter
|
||||||
import CoreData
|
import CoreData
|
||||||
import OSLog
|
import OSLog
|
||||||
import Sentry
|
|
||||||
|
|
||||||
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
|
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
static private(set) var crashReporter: PLCrashReporter!
|
||||||
|
static var pendingCrashReport: PLCrashReport?
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
configureSentry()
|
#if !DEBUG
|
||||||
swizzleStatusBar()
|
setupCrashReporter()
|
||||||
|
#endif
|
||||||
|
|
||||||
AppShortcutItem.createItems(for: application)
|
AppShortcutItem.createItems(for: application)
|
||||||
|
|
||||||
|
@ -49,37 +52,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureSentry() {
|
private func setupCrashReporter() {
|
||||||
guard let dsn = Bundle.main.object(forInfoDictionaryKey: "SentryDSN") as? String,
|
let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
|
||||||
!dsn.isEmpty else {
|
AppDelegate.crashReporter = PLCrashReporter(configuration: config)
|
||||||
return
|
|
||||||
|
if AppDelegate.crashReporter.hasPendingCrashReport(),
|
||||||
|
let data = try? AppDelegate.crashReporter.loadPendingCrashReportDataAndReturnError(),
|
||||||
|
let report = try? PLCrashReport(data: data) {
|
||||||
|
AppDelegate.crashReporter.purgePendingCrashReport()
|
||||||
|
AppDelegate.pendingCrashReport = report
|
||||||
}
|
}
|
||||||
SentrySDK.start { options in
|
|
||||||
#if DEBUG
|
|
||||||
options.debug = true
|
|
||||||
options.environment = "dev"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// the '//' in the full url can't be escaped, so we have to add the scheme back
|
AppDelegate.crashReporter.enable()
|
||||||
options.dsn = "https://\(dsn)"
|
|
||||||
|
|
||||||
options.enableSwizzling = false
|
|
||||||
// required to support releases/release health
|
|
||||||
options.enableAutoSessionTracking = true
|
|
||||||
options.enableOutOfMemoryTracking = false
|
|
||||||
options.enableAutoPerformanceTracking = false
|
|
||||||
options.enableNetworkTracking = false
|
|
||||||
options.enableAppHangTracking = false
|
|
||||||
options.enableCoreDataTracking = false
|
|
||||||
// we don't care about events like battery, keyboard show/hide
|
|
||||||
options.enableAutoBreadcrumbTracking = false
|
|
||||||
|
|
||||||
options.beforeSend = { event in
|
|
||||||
// just no, why would anyone need this information
|
|
||||||
event.context?.removeValue(forKey: "culture")
|
|
||||||
return Preferences.shared.reportErrorsAutomatically ? event : nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func buildMenu(with builder: UIMenuBuilder) {
|
override func buildMenu(with builder: UIMenuBuilder) {
|
||||||
|
@ -129,28 +113,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil)
|
UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func swizzleStatusBar() {
|
|
||||||
let selector = Selector(("handleTapAction:"))
|
|
||||||
var originalIMP: IMP?
|
|
||||||
let imp = imp_implementationWithBlock({ (self: UIStatusBarManager, sender: AnyObject) in
|
|
||||||
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIStatusBarManager, Selector, AnyObject) -> Void).self)
|
|
||||||
guard let windowScene = self.perform(Selector(("windowScene"))).takeUnretainedValue() as? UIWindowScene,
|
|
||||||
let xPosition = sender.value(forKey: "xPosition") as? CGFloat,
|
|
||||||
let delegate = windowScene.delegate as? TuskerSceneDelegate else {
|
|
||||||
original(self, selector, sender)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch delegate.handleStatusBarTapped(xPosition: xPosition) {
|
|
||||||
case .stop:
|
|
||||||
return
|
|
||||||
case .continue:
|
|
||||||
original(self, selector, sender)
|
|
||||||
}
|
|
||||||
} as @convention(block) (UIStatusBarManager, AnyObject) -> Void)
|
|
||||||
originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@")
|
|
||||||
if originalIMP == nil {
|
|
||||||
Logging.general.error("Unable to swizzle status bar manager")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,7 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
switch timeline {
|
switch timeline {
|
||||||
// todo: list/hashtag controllers need whole objects which must be fetched asynchronously
|
// todo: list/hashtag controllers need whole objects which must be fetched asynchronously
|
||||||
default:
|
default:
|
||||||
return TimelineViewController(for: timeline, mastodonController: mastodonController)
|
return TimelineTableViewController(for: timeline, mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ class DiskCache<T> {
|
||||||
let defaultExpiry: CacheExpiry
|
let defaultExpiry: CacheExpiry
|
||||||
let transformer: DiskCacheTransformer<T>
|
let transformer: DiskCacheTransformer<T>
|
||||||
|
|
||||||
private var fileStates = MultiThreadDictionary<String, FileState>()
|
private var fileStates = [String: FileState]()
|
||||||
|
|
||||||
init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer<T>, fileManager: FileManager = .default) throws {
|
init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer<T>, fileManager: FileManager = .default) throws {
|
||||||
self.defaultExpiry = defaultExpiry
|
self.defaultExpiry = defaultExpiry
|
||||||
|
@ -117,9 +117,7 @@ class DiskCache<T> {
|
||||||
func removeAll() throws {
|
func removeAll() throws {
|
||||||
try fileManager.removeItem(atPath: path)
|
try fileManager.removeItem(atPath: path)
|
||||||
try createDirectory()
|
try createDirectory()
|
||||||
fileStates.withLock { dict in
|
fileStates.removeAll()
|
||||||
dict.removeAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,10 @@ class ImageCache {
|
||||||
private let cache: ImageDataCache
|
private let cache: ImageDataCache
|
||||||
private let desiredPixelSize: CGSize?
|
private let desiredPixelSize: CGSize?
|
||||||
|
|
||||||
|
private var groups = MultiThreadDictionary<URL, RequestGroup>()
|
||||||
|
|
||||||
|
private var backgroundQueue = DispatchQueue(label: "ImageCache completion queue", qos: .default)
|
||||||
|
|
||||||
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
|
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
|
||||||
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
|
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
|
||||||
let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
|
let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
|
||||||
|
@ -37,19 +41,15 @@ class ImageCache {
|
||||||
let wrappedCompletion: ((Data?, UIImage?) -> Void)?
|
let wrappedCompletion: ((Data?, UIImage?) -> Void)?
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
wrappedCompletion = { (data, image) in
|
wrappedCompletion = { (data, image) in
|
||||||
if let image {
|
if !loadOriginal,
|
||||||
if !loadOriginal,
|
let size = self.desiredPixelSize {
|
||||||
let size = self.desiredPixelSize {
|
image?.prepareThumbnail(of: size, completionHandler: {
|
||||||
image.prepareThumbnail(of: size) {
|
completion(data, $0)
|
||||||
completion(data, $0)
|
})
|
||||||
}
|
|
||||||
} else {
|
|
||||||
image.prepareForDisplay {
|
|
||||||
completion(data, $0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
completion(data, image)
|
image?.prepareForDisplay {
|
||||||
|
completion(data, $0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -61,9 +61,14 @@ class ImageCache {
|
||||||
wrappedCompletion?(entry.data, entry.image)
|
wrappedCompletion?(entry.data, entry.image)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
let task = dataTask(url: url, completion: wrappedCompletion)
|
if let group = groups[url] {
|
||||||
task.resume()
|
return group.addCallback(wrappedCompletion)
|
||||||
return task
|
} else {
|
||||||
|
let group = createGroup(url: url)
|
||||||
|
let request = group.addCallback(wrappedCompletion)
|
||||||
|
group.run()
|
||||||
|
return request
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,23 +85,22 @@ class ImageCache {
|
||||||
// if caching is disabled, don't bother fetching since nothing will be done with the result
|
// if caching is disabled, don't bother fetching since nothing will be done with the result
|
||||||
guard !ImageCache.disableCaching else { return }
|
guard !ImageCache.disableCaching else { return }
|
||||||
|
|
||||||
if !((try? cache.has(url.absoluteString)) ?? false) {
|
if !((try? cache.has(url.absoluteString)) ?? false),
|
||||||
let task = dataTask(url: url) { data, image in
|
!groups.contains(key: url) {
|
||||||
guard let data else { return }
|
let group = createGroup(url: url)
|
||||||
try? self.cache.set(url.absoluteString, data: data, image: image)
|
group.run()
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dataTask(url: URL, completion: ((Data?, UIImage?) -> Void)?) -> URLSessionDataTask {
|
private func createGroup(url: URL) -> RequestGroup {
|
||||||
return URLSession.shared.dataTask(with: url) { data, response, error in
|
let group = RequestGroup(url: url) { (data, image) in
|
||||||
guard error == nil,
|
if let data = data {
|
||||||
let data else {
|
try? self.cache.set(url.absoluteString, data: data, image: image)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
completion?(data, UIImage(data: data))
|
_ = self.groups.removeValue(forKey: url)
|
||||||
}
|
}
|
||||||
|
groups[url] = group
|
||||||
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
func getData(_ url: URL) -> Data? {
|
func getData(_ url: URL) -> Data? {
|
||||||
|
@ -107,10 +111,87 @@ class ImageCache {
|
||||||
return try? cache.get(url.absoluteString, loadOriginal: loadOriginal)
|
return try? cache.get(url.absoluteString, loadOriginal: loadOriginal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancelWithoutCallback(_ url: URL) {
|
||||||
|
groups[url]?.cancelWithoutCallback()
|
||||||
|
}
|
||||||
|
|
||||||
func reset() throws {
|
func reset() throws {
|
||||||
try cache.removeAll()
|
try cache.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Request = URLSessionDataTask
|
private class RequestGroup {
|
||||||
|
let url: URL
|
||||||
|
private let onFinished: (Data?, UIImage?) -> Void
|
||||||
|
private var task: URLSessionDataTask?
|
||||||
|
private var requests = [Request]()
|
||||||
|
|
||||||
|
init(url: URL, onFinished: @escaping (Data?, UIImage?) -> Void) {
|
||||||
|
self.url = url
|
||||||
|
self.onFinished = onFinished
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
task?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
|
||||||
|
guard error == nil, let data = data else {
|
||||||
|
self.complete(with: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.complete(with: data)
|
||||||
|
})
|
||||||
|
task!.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCallback(_ completion: ((Data?, UIImage?) -> Void)?) -> Request {
|
||||||
|
let request = Request(callback: completion)
|
||||||
|
requests.append(request)
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelWithoutCallback() {
|
||||||
|
if let request = requests.first(where: { $0.callback == nil && !$0.cancelled }) {
|
||||||
|
request.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func requestCancelled() {
|
||||||
|
let remaining = requests.filter { !$0.cancelled }.count
|
||||||
|
if remaining <= 0 {
|
||||||
|
task?.cancel()
|
||||||
|
complete(with: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func complete(with data: Data?) {
|
||||||
|
let image = data != nil ? UIImage(data: data!) : nil
|
||||||
|
|
||||||
|
requests.filter { !$0.cancelled }.forEach {
|
||||||
|
if let callback = $0.callback {
|
||||||
|
callback(data, image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onFinished(data, image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Request {
|
||||||
|
private weak var group: RequestGroup?
|
||||||
|
private(set) var callback: ((Data?, UIImage?) -> Void)?
|
||||||
|
private(set) var cancelled: Bool = false
|
||||||
|
|
||||||
|
init(callback: ((Data?, UIImage?) -> Void)?) {
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
cancelled = true
|
||||||
|
callback = nil
|
||||||
|
group?.requestCancelled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import CoreData
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
import OSLog
|
import OSLog
|
||||||
import Sentry
|
|
||||||
|
|
||||||
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
|
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
|
||||||
|
|
||||||
|
@ -36,9 +35,6 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
return context
|
return context
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// TODO: consider sending managed objects through this to avoid re-fetching things unnecessarily
|
|
||||||
// would need to audit existing uses to make sure everything happens on the main thread
|
|
||||||
// and when updating things on the background context would need to switch to main, refetch, and then publish
|
|
||||||
let statusSubject = PassthroughSubject<String, Never>()
|
let statusSubject = PassthroughSubject<String, Never>()
|
||||||
let accountSubject = PassthroughSubject<String, Never>()
|
let accountSubject = PassthroughSubject<String, Never>()
|
||||||
let relationshipSubject = PassthroughSubject<String, Never>()
|
let relationshipSubject = PassthroughSubject<String, Never>()
|
||||||
|
@ -74,9 +70,6 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
try context.save()
|
try context.save()
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Unable to save managed object context: \(String(describing: error), privacy: .public)")
|
logger.error("Unable to save managed object context: \(String(describing: error), privacy: .public)")
|
||||||
let crumb = Breadcrumb(level: .fatal, category: "PersistentStore")
|
|
||||||
crumb.message = String(describing: error)
|
|
||||||
SentrySDK.addBreadcrumb(crumb: crumb)
|
|
||||||
fatalError("Unable to save managed object context")
|
fatalError("Unable to save managed object context")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,20 +144,19 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func upsert(account: Account, in context: NSManagedObjectContext) -> AccountMO {
|
private func upsert(account: Account) -> AccountMO {
|
||||||
if let accountMO = self.account(for: account.id, in: context) {
|
if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
|
||||||
accountMO.updateFrom(apiAccount: account, container: self)
|
accountMO.updateFrom(apiAccount: account, container: self)
|
||||||
return accountMO
|
return accountMO
|
||||||
} else {
|
} else {
|
||||||
return AccountMO(apiAccount: account, container: self, context: context)
|
return AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addOrUpdate(account: Account, in context: NSManagedObjectContext? = nil, completion: ((AccountMO) -> Void)? = nil) {
|
func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) {
|
||||||
let context = context ?? backgroundContext
|
backgroundContext.perform {
|
||||||
context.perform {
|
let accountMO = self.upsert(account: account)
|
||||||
let accountMO = self.upsert(account: account, in: context)
|
self.save(context: self.backgroundContext)
|
||||||
self.save(context: context)
|
|
||||||
completion?(accountMO)
|
completion?(accountMO)
|
||||||
self.accountSubject.send(account.id)
|
self.accountSubject.send(account.id)
|
||||||
}
|
}
|
||||||
|
@ -183,21 +175,20 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func upsert(relationship: Relationship, in context: NSManagedObjectContext) -> RelationshipMO {
|
private func upsert(relationship: Relationship) -> RelationshipMO {
|
||||||
if let relationshipMO = self.relationship(forAccount: relationship.id, in: context) {
|
if let relationshipMO = self.relationship(forAccount: relationship.id, in: self.backgroundContext) {
|
||||||
relationshipMO.updateFrom(apiRelationship: relationship, container: self)
|
relationshipMO.updateFrom(apiRelationship: relationship, container: self)
|
||||||
return relationshipMO
|
return relationshipMO
|
||||||
} else {
|
} else {
|
||||||
let relationshipMO = RelationshipMO(apiRelationship: relationship, container: self, context: context)
|
let relationshipMO = RelationshipMO(apiRelationship: relationship, container: self, context: self.backgroundContext)
|
||||||
return relationshipMO
|
return relationshipMO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addOrUpdate(relationship: Relationship, in context: NSManagedObjectContext? = nil, completion: ((RelationshipMO) -> Void)? = nil) {
|
func addOrUpdate(relationship: Relationship, completion: ((RelationshipMO) -> Void)? = nil) {
|
||||||
let context = context ?? backgroundContext
|
backgroundContext.perform {
|
||||||
context.perform {
|
let relationshipMO = self.upsert(relationship: relationship)
|
||||||
let relationshipMO = self.upsert(relationship: relationship, in: context)
|
self.save(context: self.backgroundContext)
|
||||||
self.save(context: context)
|
|
||||||
completion?(relationshipMO)
|
completion?(relationshipMO)
|
||||||
self.relationshipSubject.send(relationship.id)
|
self.relationshipSubject.send(relationship.id)
|
||||||
}
|
}
|
||||||
|
@ -205,7 +196,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
|
|
||||||
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
|
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
accounts.forEach { self.upsert(account: $0) }
|
||||||
self.save(context: self.backgroundContext)
|
self.save(context: self.backgroundContext)
|
||||||
completion?()
|
completion?()
|
||||||
accounts.forEach { self.accountSubject.send($0.id) }
|
accounts.forEach { self.accountSubject.send($0.id) }
|
||||||
|
@ -219,7 +210,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
// since the status has the same account as the notification
|
// since the status has the same account as the notification
|
||||||
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
||||||
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
accounts.forEach { self.upsert(account: $0) }
|
||||||
self.save(context: self.backgroundContext)
|
self.save(context: self.backgroundContext)
|
||||||
completion?()
|
completion?()
|
||||||
statuses.forEach { self.statusSubject.send($0.id) }
|
statuses.forEach { self.statusSubject.send($0.id) }
|
||||||
|
@ -233,7 +224,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
var updatedStatuses = [String]()
|
var updatedStatuses = [String]()
|
||||||
|
|
||||||
block(self.backgroundContext, { (accounts) in
|
block(self.backgroundContext, { (accounts) in
|
||||||
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
accounts.forEach { self.upsert(account: $0) }
|
||||||
updatedAccounts.append(contentsOf: accounts.map { $0.id })
|
updatedAccounts.append(contentsOf: accounts.map { $0.id })
|
||||||
}, { (statuses) in
|
}, { (statuses) in
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<array>
|
<array>
|
||||||
<string>counter\.social</string>
|
|
||||||
<string>gab\..+</string>
|
<string>gab\..+</string>
|
||||||
</array>
|
</array>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
//
|
|
||||||
// UIScrollView+Top.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/1/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
private var prevScrollOffsetBeforeScrollToTopKey: Void = ()
|
|
||||||
|
|
||||||
extension UIScrollView {
|
|
||||||
private var prevScrollOffsetBeforeScrollToTop: CGFloat? {
|
|
||||||
get {
|
|
||||||
if let v = (objc_getAssociatedObject(self, &prevScrollOffsetBeforeScrollToTopKey) as? NSNumber)?.doubleValue {
|
|
||||||
return CGFloat(v)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
if let newValue {
|
|
||||||
objc_setAssociatedObject(self, &prevScrollOffsetBeforeScrollToTopKey, NSNumber(value: newValue), .OBJC_ASSOCIATION_COPY_NONATOMIC)
|
|
||||||
} else {
|
|
||||||
objc_setAssociatedObject(self, &prevScrollOffsetBeforeScrollToTopKey, nil, .OBJC_ASSOCIATION_COPY_NONATOMIC)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollToTop() {
|
|
||||||
let top = -adjustedContentInset.top
|
|
||||||
// +5 to add a little bit of wiggle room
|
|
||||||
let isScrolledToTop = contentOffset.y < top + 5
|
|
||||||
if isScrolledToTop {
|
|
||||||
if let prevScrollOffsetBeforeScrollToTop {
|
|
||||||
self.prevScrollOffsetBeforeScrollToTop = nil
|
|
||||||
setContentOffset(CGPoint(x: 0, y: prevScrollOffsetBeforeScrollToTop), animated: true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
prevScrollOffsetBeforeScrollToTop = contentOffset.y
|
|
||||||
setContentOffset(CGPoint(x: 0, y: top), animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,6 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>SentryDSN</key>
|
|
||||||
<string>$(SENTRY_DSN)</string>
|
|
||||||
<key>OSLogPreferences</key>
|
<key>OSLogPreferences</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>space.vaccor.Tusker</key>
|
<key>space.vaccor.Tusker</key>
|
||||||
|
|
|
@ -17,13 +17,12 @@ struct Logging {
|
||||||
static func getLogData() -> Data? {
|
static func getLogData() -> Data? {
|
||||||
do {
|
do {
|
||||||
let store = try OSLogStore(scope: .currentProcessIdentifier)
|
let store = try OSLogStore(scope: .currentProcessIdentifier)
|
||||||
// do the filtering ourself, passing position/predicate into getEntries is far slower (priority inversion, I think)
|
// past hour
|
||||||
let entries = try store.getEntries()
|
let position = store.position(date: Date().addingTimeInterval(-60 * 60))
|
||||||
|
let entries = try store.getEntries(at: position, matching: NSPredicate(format: "subsystem = %@", Bundle.main.bundleIdentifier!))
|
||||||
var data = Data()
|
var data = Data()
|
||||||
let subsystem = Bundle.main.bundleIdentifier!
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
guard let entry = entry as? OSLogEntryLog,
|
guard let entry = entry as? OSLogEntryLog else {
|
||||||
entry.subsystem == subsystem else {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data.append(contentsOf: entry.date.formatted(.iso8601).utf8)
|
data.append(contentsOf: entry.date.formatted(.iso8601).utf8)
|
||||||
|
@ -40,23 +39,4 @@ struct Logging {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func writeDataForCrash() {
|
|
||||||
guard let data = getLogData(),
|
|
||||||
let cacheDir = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let timestamp = ISO8601DateFormatter().string(from: Date())
|
|
||||||
let url = cacheDir.appendingPathComponent("Tusker-\(timestamp).log", isDirectory: false)
|
|
||||||
do {
|
|
||||||
try data.write(to: url)
|
|
||||||
UserDefaults.standard.set(url, forKey: "lastCrashLog")
|
|
||||||
} catch {
|
|
||||||
// if we can't write the data, oh well, we just won't have logs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func logURLForLastCrash() -> URL? {
|
|
||||||
return UserDefaults.standard.url(forKey: "lastCrashLog")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,11 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import CrashReporter
|
||||||
import MessageUI
|
import MessageUI
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate {
|
class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
|
@ -31,10 +32,15 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
|
|
||||||
window = UIWindow(windowScene: windowScene)
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
|
||||||
|
if let report = AppDelegate.pendingCrashReport {
|
||||||
|
AppDelegate.pendingCrashReport = nil
|
||||||
|
handlePendingCrashReport(report, session: session)
|
||||||
|
} else {
|
||||||
showAppOrOnboardingUI(session: session)
|
showAppOrOnboardingUI(session: session)
|
||||||
if connectionOptions.urlContexts.count > 0 {
|
if connectionOptions.urlContexts.count > 0 {
|
||||||
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window!.makeKeyAndVisible()
|
window!.makeKeyAndVisible()
|
||||||
|
|
||||||
|
@ -136,6 +142,19 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handlePendingCrashReport(_ report: PLCrashReport, session: UISceneSession) {
|
||||||
|
#if !DEBUG
|
||||||
|
guard MFMailComposeViewController.canSendMail() else {
|
||||||
|
print("Cannot send email")
|
||||||
|
showAppOrOnboardingUI(session: session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window!.rootViewController = CrashReporterViewController.create(report: report, dismiss: {
|
||||||
|
self.showAppOrOnboardingUI()
|
||||||
|
})
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
|
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
|
||||||
let session = session ?? window!.windowScene!.session
|
let session = session ?? window!.windowScene!.session
|
|
@ -62,7 +62,7 @@ class Draft: Codable, ObservableObject {
|
||||||
self.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments)
|
self.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments)
|
||||||
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
|
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
|
||||||
self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility)
|
self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility)
|
||||||
self.poll = try container.decode(Poll?.self, forKey: .poll)
|
self.poll = try container.decode(Poll.self, forKey: .poll)
|
||||||
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly) ?? false
|
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly) ?? false
|
||||||
|
|
||||||
self.initialText = try container.decode(String.self, forKey: .initialText)
|
self.initialText = try container.decode(String.self, forKey: .initialText)
|
||||||
|
|
|
@ -34,39 +34,21 @@ class DraftsManager: Codable {
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
var drafts: [Draft] = []
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
if let dict = try? container.decode([UUID: Draft].self, forKey: .drafts) {
|
|
||||||
self.drafts = dict
|
|
||||||
} else if let array = try? container.decode([Draft].self, forKey: .drafts) {
|
|
||||||
self.drafts = array.reduce(into: [:], { partialResult, draft in
|
|
||||||
partialResult[draft.id] = draft
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: .drafts, in: container, debugDescription: "expected drafts to be a dict or array of drafts")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var drafts: [UUID: Draft] = [:]
|
|
||||||
var sorted: [Draft] {
|
var sorted: [Draft] {
|
||||||
return drafts.values.sorted(by: { $0.lastModified > $1.lastModified })
|
return drafts.sorted(by: { $0.lastModified > $1.lastModified })
|
||||||
}
|
}
|
||||||
|
|
||||||
func add(_ draft: Draft) {
|
func add(_ draft: Draft) {
|
||||||
drafts[draft.id] = draft
|
drafts.append(draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(_ draft: Draft) {
|
func remove(_ draft: Draft) {
|
||||||
drafts.removeValue(forKey: draft.id)
|
drafts.removeAll { $0 == draft }
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBy(id: UUID) -> Draft? {
|
func getBy(id: UUID) -> Draft? {
|
||||||
return drafts[id]
|
return drafts.first { $0.id == id }
|
||||||
}
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case drafts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,13 @@ class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
||||||
private let lock: LockHolder<[AnyHashable: Any]>
|
private let lock: LockHolder<[AnyHashable: Any]>
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.lock = LockHolder(initialState: [:])
|
if #available(iOS 16.0, *) {
|
||||||
|
let lock = OSAllocatedUnfairLock(initialState: [:])
|
||||||
|
self.lock = LockHolder(withLock: lock.withLock(_:))
|
||||||
|
} else {
|
||||||
|
let lock = UnfairLock(initialState: [:])
|
||||||
|
self.lock = LockHolder(withLock: lock.withLock(_:))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript(key: Key) -> Value? {
|
subscript(key: Key) -> Value? {
|
||||||
|
@ -60,16 +66,6 @@ class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> {
|
||||||
// see #178
|
// see #178
|
||||||
fileprivate struct LockHolder<State> {
|
fileprivate struct LockHolder<State> {
|
||||||
let withLock: (_ body: @Sendable (inout State) throws -> any Sendable) throws -> any Sendable
|
let withLock: (_ body: @Sendable (inout State) throws -> any Sendable) throws -> any Sendable
|
||||||
|
|
||||||
init(initialState: State) {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
let lock = OSAllocatedUnfairLock(initialState: initialState)
|
|
||||||
self.withLock = lock.withLock(_:)
|
|
||||||
} else {
|
|
||||||
let lock = UnfairLock(initialState: initialState)
|
|
||||||
self.withLock = lock.withLock(_:)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace this only with OSAllocatedUnfairLock
|
// TODO: replace this only with OSAllocatedUnfairLock
|
||||||
|
|
|
@ -67,6 +67,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
||||||
self.hideDiscover = try container.decodeIfPresent(Bool.self, forKey: .hideDiscover) ?? false
|
self.hideDiscover = try container.decodeIfPresent(Bool.self, forKey: .hideDiscover) ?? false
|
||||||
|
|
||||||
|
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.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false
|
||||||
|
@ -106,6 +107,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
||||||
try container.encode(hideDiscover, forKey: .hideDiscover)
|
try container.encode(hideDiscover, forKey: .hideDiscover)
|
||||||
|
|
||||||
|
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(hasShownLocalTimelineDescription, forKey: .hasShownLocalTimelineDescription)
|
||||||
|
@ -148,8 +150,8 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var hideDiscover = false
|
@Published var hideDiscover = false
|
||||||
|
|
||||||
// MARK: Advanced
|
// MARK: Advanced
|
||||||
|
@Published var silentActions: [String: Permission] = [:]
|
||||||
@Published var statusContentType: StatusContentType = .plain
|
@Published var statusContentType: StatusContentType = .plain
|
||||||
@Published var reportErrorsAutomatically = true
|
|
||||||
|
|
||||||
// MARK:
|
// MARK:
|
||||||
@Published var hasShownLocalTimelineDescription = false
|
@Published var hasShownLocalTimelineDescription = false
|
||||||
|
@ -186,6 +188,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case disableInfiniteScrolling
|
case disableInfiniteScrolling
|
||||||
case hideDiscover
|
case hideDiscover
|
||||||
|
|
||||||
|
case silentActions
|
||||||
case statusContentType
|
case statusContentType
|
||||||
|
|
||||||
case hasShownLocalTimelineDescription
|
case hasShownLocalTimelineDescription
|
||||||
|
@ -194,4 +197,10 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Preferences {
|
||||||
|
enum Permission: String, Codable {
|
||||||
|
case undecided, accepted, rejected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension UIUserInterfaceStyle: Codable {}
|
extension UIUserInterfaceStyle: Codable {}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
//
|
|
||||||
// TuskerSceneDelegate.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 10/31/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
protocol TuskerSceneDelegate: UISceneDelegate {
|
|
||||||
var rootViewController: TuskerRootViewController? { get }
|
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult
|
|
||||||
}
|
|
||||||
|
|
||||||
enum StatusBarTapActionResult {
|
|
||||||
case `continue`
|
|
||||||
case stop
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TuskerSceneDelegate {
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
if let rootViewController {
|
|
||||||
let converted = rootViewController.view.convert(CGPoint(x: xPosition, y: 0), from: nil)
|
|
||||||
return rootViewController.handleStatusBarTapped(xPosition: converted.x)
|
|
||||||
}
|
|
||||||
return .continue
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -65,7 +65,7 @@ class AccountListTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountListTableViewController: TuskerNavigationDelegate {
|
extension AccountListTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountListTableViewController: ToastableViewController {
|
extension AccountListTableViewController: ToastableViewController {
|
||||||
|
|
|
@ -154,7 +154,6 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BookmarksTableViewController: TuskerNavigationDelegate {
|
extension BookmarksTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BookmarksTableViewController: ToastableViewController {
|
extension BookmarksTableViewController: ToastableViewController {
|
||||||
|
@ -164,6 +163,8 @@ extension BookmarksTableViewController: MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
extension BookmarksTableViewController: StatusTableViewCellDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
|
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
tableView.beginUpdates()
|
tableView.beginUpdates()
|
||||||
tableView.endUpdates()
|
tableView.endUpdates()
|
||||||
|
@ -175,4 +176,14 @@ extension BookmarksTableViewController: UITableViewDataSourcePrefetching, Status
|
||||||
let ids = indexPaths.map { statuses[$0.row].id }
|
let ids = indexPaths.map { statuses[$0.row].id }
|
||||||
prefetchStatuses(with: ids)
|
prefetchStatuses(with: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
|
let ids: [String] = indexPaths.compactMap {
|
||||||
|
guard $0.row < statuses.count else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return statuses[$0.row].id
|
||||||
|
}
|
||||||
|
cancelPrefetchingStatuses(with: ids)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,6 +234,7 @@ struct SheetOrPopover<V: View>: ViewModifier {
|
||||||
@Environment(\.horizontalSizeClass) var sizeClass
|
@Environment(\.horizontalSizeClass) var sizeClass
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
let _ = print("isPresented: \(isPresented)")
|
||||||
if sizeClass == .compact {
|
if sizeClass == .compact {
|
||||||
content.sheet(isPresented: $isPresented, content: view)
|
content.sheet(isPresented: $isPresented, content: view)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -137,7 +137,8 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
MainComposeTextView(
|
MainComposeTextView(
|
||||||
draft: draft
|
draft: draft,
|
||||||
|
placeholder: Text("What's on your mind?")
|
||||||
)
|
)
|
||||||
|
|
||||||
if let poll = draft.poll {
|
if let poll = draft.poll {
|
||||||
|
|
|
@ -11,23 +11,7 @@ import Pachyderm
|
||||||
|
|
||||||
struct MainComposeTextView: View {
|
struct MainComposeTextView: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@State private var placeholder: Text = {
|
let placeholder: Text
|
||||||
let components = Calendar.current.dateComponents([.month, .day], from: Date())
|
|
||||||
if components.month == 3 && components.day == 14 {
|
|
||||||
if Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
|
|
||||||
return Text("Happy π day!")
|
|
||||||
}
|
|
||||||
} else if components.month == 9 && components.day == 21 {
|
|
||||||
return Text("Do you remember?")
|
|
||||||
} else if components.month == 10 && components.day == 31 {
|
|
||||||
if .random() {
|
|
||||||
return Text("Post something spooky!")
|
|
||||||
} else {
|
|
||||||
return Text("Any questions?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Text("What's on your mind?")
|
|
||||||
}()
|
|
||||||
|
|
||||||
let minHeight: CGFloat = 150
|
let minHeight: CGFloat = 150
|
||||||
@State private var height: CGFloat?
|
@State private var height: CGFloat?
|
||||||
|
|
|
@ -441,8 +441,6 @@ extension ConversationTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConversationTableViewController: TuskerNavigationDelegate {
|
extension ConversationTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
|
||||||
|
|
||||||
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController {
|
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController {
|
||||||
let vc = ConversationTableViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
let vc = ConversationTableViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
||||||
// transfer show statuses automatically state when showing new conversation
|
// transfer show statuses automatically state when showing new conversation
|
||||||
|
@ -455,6 +453,7 @@ extension ConversationTableViewController: MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
// causes the table view to recalculate the cell heights
|
// causes the table view to recalculate the cell heights
|
||||||
tableView.beginUpdates()
|
tableView.beginUpdates()
|
||||||
|
@ -467,6 +466,11 @@ extension ConversationTableViewController: UITableViewDataSourcePrefetching, Sta
|
||||||
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||||
prefetchStatuses(with: ids)
|
prefetchStatuses(with: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
|
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||||
|
cancelPrefetchingStatuses(with: ids)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConversationTableViewController: ToastableViewController {
|
extension ConversationTableViewController: ToastableViewController {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
//
|
||||||
|
// CrashReporterViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/29/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import CrashReporter
|
||||||
|
|
||||||
|
class CrashReporterViewController: IssueReporterViewController {
|
||||||
|
|
||||||
|
private let report: PLCrashReport
|
||||||
|
|
||||||
|
override var preamble: String {
|
||||||
|
"Tusker has detected that it crashed the last time it was running. You can email the report to the developer or skip sending and continue to the app. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior to the crash that may be pertinent."
|
||||||
|
}
|
||||||
|
|
||||||
|
override var subject: String {
|
||||||
|
"Tusker Crash Report"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func create(report: PLCrashReport, dismiss: @escaping () -> Void) -> UINavigationController {
|
||||||
|
return create(CrashReporterViewController(report: report, dismiss: dismiss))
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(report: PLCrashReport, dismiss: @escaping () -> Void) {
|
||||||
|
self.report = report
|
||||||
|
let reportText = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)!
|
||||||
|
let timestamp = ISO8601DateFormatter().string(from: report.systemInfo.timestamp)
|
||||||
|
let reportFilename = "Tusker-crash-\(timestamp).crash"
|
||||||
|
|
||||||
|
super.init(reportText: reportText, reportFilename: reportFilename, dismiss: dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
navigationItem.title = NSLocalizedString("Crash Detected", comment: "crash reporter title")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import CrashReporter
|
||||||
import MessageUI
|
import MessageUI
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ class IssueReporterViewController: UIViewController {
|
||||||
|
|
||||||
let reportText: String
|
let reportText: String
|
||||||
let reportFilename: String
|
let reportFilename: String
|
||||||
private let doDismiss: () -> Void
|
private let dismiss: () -> Void
|
||||||
|
|
||||||
var preamble: String {
|
var preamble: String {
|
||||||
"Tusker has encountered an error. You can email a report to the developer. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior that may be pertinent."
|
"Tusker has encountered an error. You can email a report to the developer. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior that may be pertinent."
|
||||||
|
@ -42,7 +43,7 @@ class IssueReporterViewController: UIViewController {
|
||||||
init(reportText: String, reportFilename: String, dismiss: @escaping () -> Void) {
|
init(reportText: String, reportFilename: String, dismiss: @escaping () -> Void) {
|
||||||
self.reportText = reportText
|
self.reportText = reportText
|
||||||
self.reportFilename = reportFilename
|
self.reportFilename = reportFilename
|
||||||
self.doDismiss = dismiss
|
self.dismiss = dismiss
|
||||||
|
|
||||||
self.logDataTask = Task(priority: .userInitiated) {
|
self.logDataTask = Task(priority: .userInitiated) {
|
||||||
return await withCheckedContinuation({ continuation in
|
return await withCheckedContinuation({ continuation in
|
||||||
|
@ -117,7 +118,6 @@ class IssueReporterViewController: UIViewController {
|
||||||
|
|
||||||
@IBAction func sendReportTouchUpInside(_ sender: Any) {
|
@IBAction func sendReportTouchUpInside(_ sender: Any) {
|
||||||
updateSendReportButtonColor(lightened: false, animate: true)
|
updateSendReportButtonColor(lightened: false, animate: true)
|
||||||
sendReportButton.isEnabled = false
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
let composeVC = MFMailComposeViewController()
|
let composeVC = MFMailComposeViewController()
|
||||||
|
@ -128,13 +128,12 @@ class IssueReporterViewController: UIViewController {
|
||||||
let data = reportText.data(using: .utf8)!
|
let data = reportText.data(using: .utf8)!
|
||||||
composeVC.addAttachmentData(data, mimeType: "text/plain", fileName: reportFilename)
|
composeVC.addAttachmentData(data, mimeType: "text/plain", fileName: reportFilename)
|
||||||
|
|
||||||
if let (logData, name) = await getLogData() {
|
if let logData = await logDataTask.value {
|
||||||
composeVC.addAttachmentData(logData, mimeType: "text/plain", fileName: name)
|
let timestamp = ISO8601DateFormatter().string(from: Date())
|
||||||
|
composeVC.addAttachmentData(logData, mimeType: "text/plain", fileName: "Tusker-\(timestamp).log")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.present(composeVC, animated: true)
|
self.present(composeVC, animated: true)
|
||||||
|
|
||||||
sendReportButton.isEnabled = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,24 +142,11 @@ class IssueReporterViewController: UIViewController {
|
||||||
let url = dir.appendingPathComponent(reportFilename)
|
let url = dir.appendingPathComponent(reportFilename)
|
||||||
try! reportText.data(using: .utf8)!.write(to: url)
|
try! reportText.data(using: .utf8)!.write(to: url)
|
||||||
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
||||||
activityController.popoverPresentationController?.sourceView = sendReportButton
|
|
||||||
present(activityController, animated: true)
|
present(activityController, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func cancelPressed(_ sender: Any) {
|
@IBAction func cancelPressed(_ sender: Any) {
|
||||||
self.finishedReport()
|
dismiss()
|
||||||
doDismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLogData() async -> (Data, String)? {
|
|
||||||
guard let data = await logDataTask.value else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let timestamp = ISO8601DateFormatter().string(from: Date())
|
|
||||||
return (data, "Tusker-\(timestamp).log")
|
|
||||||
}
|
|
||||||
|
|
||||||
func finishedReport() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -168,12 +154,7 @@ class IssueReporterViewController: UIViewController {
|
||||||
extension IssueReporterViewController: MFMailComposeViewControllerDelegate {
|
extension IssueReporterViewController: MFMailComposeViewControllerDelegate {
|
||||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||||
controller.dismiss(animated: true) {
|
controller.dismiss(animated: true) {
|
||||||
if result == .cancelled {
|
self.dismiss()
|
||||||
// don't dismiss ourself, to allowe the user to send the report a different way
|
|
||||||
} else {
|
|
||||||
self.finishedReport()
|
|
||||||
self.doDismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ extension ProfileDirectoryViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileDirectoryViewController: TuskerNavigationDelegate {
|
extension ProfileDirectoryViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileDirectoryViewController: ToastableViewController {
|
extension ProfileDirectoryViewController: ToastableViewController {
|
||||||
|
|
|
@ -120,7 +120,7 @@ extension TrendingHashtagsViewController: UICollectionViewDragDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: TuskerNavigationDelegate {
|
extension TrendingHashtagsViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: ToastableViewController {
|
extension TrendingHashtagsViewController: ToastableViewController {
|
||||||
|
|
|
@ -100,17 +100,13 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||||
guard let hash = card.blurhash else {
|
guard let hash = card.blurhash else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let imageViewSize = self.thumbnailView.bounds.size
|
||||||
AttachmentView.queue.async { [weak self] in
|
AttachmentView.queue.async { [weak self] in
|
||||||
let size: CGSize
|
let size: CGSize
|
||||||
if let width = card.width, let height = card.height {
|
if let width = card.width, let height = card.height {
|
||||||
let aspectRatio = CGFloat(width) / CGFloat(height)
|
size = CGSize(width: width, height: height)
|
||||||
if aspectRatio > 1 {
|
|
||||||
size = CGSize(width: 32, height: 32 / aspectRatio)
|
|
||||||
} else {
|
|
||||||
size = CGSize(width: 32 * aspectRatio, height: 32)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
size = CGSize(width: 32, height: 32)
|
size = imageViewSize
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let preview = UIImage(blurHash: hash, size: size) else {
|
guard let preview = UIImage(blurHash: hash, size: size) else {
|
||||||
|
|
|
@ -143,17 +143,13 @@ class TrendingLinkTableViewCell: UITableViewCell {
|
||||||
guard let hash = card.blurhash else {
|
guard let hash = card.blurhash else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let imageViewSize = self.thumbnailView.bounds.size
|
||||||
AttachmentView.queue.async { [weak self] in
|
AttachmentView.queue.async { [weak self] in
|
||||||
let size: CGSize
|
let size: CGSize
|
||||||
if let width = card.width, let height = card.height {
|
if let width = card.width, let height = card.height {
|
||||||
let aspectRatio = CGFloat(width) / CGFloat(height)
|
size = CGSize(width: width, height: height)
|
||||||
if aspectRatio > 1 {
|
|
||||||
size = CGSize(width: 32, height: 32 / aspectRatio)
|
|
||||||
} else {
|
|
||||||
size = CGSize(width: 32 * aspectRatio, height: 32)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
size = CGSize(width: 32, height: 32)
|
size = imageViewSize
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let preview = UIImage(blurHash: hash, size: size) else {
|
guard let preview = UIImage(blurHash: hash, size: size) else {
|
||||||
|
|
|
@ -101,7 +101,7 @@ extension TrendingLinksViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingLinksViewController: TuskerNavigationDelegate {
|
extension TrendingLinksViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingLinksViewController: ToastableViewController {
|
extension TrendingLinksViewController: ToastableViewController {
|
||||||
|
|
|
@ -82,7 +82,7 @@ extension TrendingStatusesViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingStatusesViewController: TuskerNavigationDelegate {
|
extension TrendingStatusesViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingStatusesViewController: ToastableViewController {
|
extension TrendingStatusesViewController: ToastableViewController {
|
||||||
|
|
|
@ -37,14 +37,7 @@ class FindInstanceViewController: InstanceSelectorTableViewController {
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func cancelButtonPressed() {
|
@objc func cancelButtonPressed() {
|
||||||
// when the search controller is active, dismiss exits it rather than dismissing ourself, so we have to dismiss twice
|
dismiss(animated: true)
|
||||||
if searchController.isActive {
|
|
||||||
dismiss(animated: false) {
|
|
||||||
self.dismiss(animated: true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dismiss(animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import VisionKit
|
||||||
protocol LargeImageContentView: UIView {
|
protocol LargeImageContentView: UIView {
|
||||||
var animationImage: UIImage? { get }
|
var animationImage: UIImage? { get }
|
||||||
var activityItemsForSharing: [Any] { get }
|
var activityItemsForSharing: [Any] { get }
|
||||||
func setControlsVisible(_ controlsVisible: Bool)
|
|
||||||
func grayscaleStateChanged()
|
func grayscaleStateChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,9 +22,6 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
||||||
#if !targetEnvironment(macCatalyst)
|
#if !targetEnvironment(macCatalyst)
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
private static let analyzer = ImageAnalyzer()
|
private static let analyzer = ImageAnalyzer()
|
||||||
private var _analysisInteraction: AnyObject?
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction }
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var animationImage: UIImage? { image! }
|
var animationImage: UIImage? { image! }
|
||||||
|
@ -49,7 +45,6 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
||||||
if #available(iOS 16.0, *),
|
if #available(iOS 16.0, *),
|
||||||
ImageAnalyzer.isSupported {
|
ImageAnalyzer.isSupported {
|
||||||
let interaction = ImageAnalysisInteraction()
|
let interaction = ImageAnalysisInteraction()
|
||||||
self._analysisInteraction = interaction
|
|
||||||
interaction.delegate = self
|
interaction.delegate = self
|
||||||
interaction.preferredInteractionTypes = .automatic
|
interaction.preferredInteractionTypes = .automatic
|
||||||
addInteraction(interaction)
|
addInteraction(interaction)
|
||||||
|
@ -69,17 +64,6 @@ class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool) {
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
if #available(iOS 16.0, *),
|
|
||||||
let analysisInteraction {
|
|
||||||
// note: passing animated: true here doesn't seem to do anything by itself as of iOS 16.2 (20C5032e)
|
|
||||||
// so the LargeImageViewController handles animating, but we still need to pass true here otherwise it doesn't animate
|
|
||||||
analysisInteraction.setSupplementaryInterfaceHidden(!controlsVisible, animated: true)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
func grayscaleStateChanged() {
|
||||||
guard let data = sourceData else {
|
guard let data = sourceData else {
|
||||||
return
|
return
|
||||||
|
@ -129,9 +113,6 @@ class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
func grayscaleStateChanged() {
|
||||||
// todo
|
// todo
|
||||||
}
|
}
|
||||||
|
@ -170,9 +151,6 @@ class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
func grayscaleStateChanged() {
|
||||||
// no-op, GifvAttachmentView observes the grayscale state itself
|
// no-op, GifvAttachmentView observes the grayscale state itself
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,7 +174,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
self.controlsVisible = controlsVisible
|
self.controlsVisible = controlsVisible
|
||||||
if animated {
|
if animated {
|
||||||
UIView.animate(withDuration: 0.2) {
|
UIView.animate(withDuration: 0.2) {
|
||||||
self.contentView.setControlsVisible(controlsVisible)
|
|
||||||
self.updateControlsView()
|
self.updateControlsView()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -174,7 +174,7 @@ extension EditListAccountsViewController: SearchResultsViewControllerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EditListAccountsViewController: TuskerNavigationDelegate {
|
extension EditListAccountsViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EditListAccountsViewController: ToastableViewController {
|
extension EditListAccountsViewController: ToastableViewController {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class ListTimelineViewController: TimelineViewController {
|
class ListTimelineViewController: TimelineTableViewController {
|
||||||
|
|
||||||
let list: List
|
let list: List
|
||||||
|
|
||||||
|
@ -57,11 +57,8 @@ class ListTimelineViewController: TimelineViewController {
|
||||||
@objc func editListDoneButtonPressed() {
|
@objc func editListDoneButtonPressed() {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
|
|
||||||
// TODO: only reload if there were changes
|
// todo: show loading indicator
|
||||||
Task {
|
reloadInitial()
|
||||||
applyInitialSnapshot()
|
|
||||||
await controller.loadInitial()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,12 +111,6 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
root.presentPreferences(completion: completion)
|
root.presentPreferences(completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
loadViewIfNeeded()
|
|
||||||
// TODO: check if fast account switcher is being presented?
|
|
||||||
return root.handleStatusBarTapped(xPosition: xPosition)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -456,22 +456,6 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
func presentPreferences(completion: (() -> Void)?) {
|
||||||
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
guard presentedViewController == nil else {
|
|
||||||
return .stop
|
|
||||||
}
|
|
||||||
if traitCollection.horizontalSizeClass == .compact {
|
|
||||||
return tabBarViewController.handleStatusBarTapped(xPosition: xPosition)
|
|
||||||
} else {
|
|
||||||
let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view)
|
|
||||||
if secondaryNavController.view.bounds.contains(pointInSecondary) {
|
|
||||||
return secondaryNavController.handleStatusBarTapped(xPosition: pointInSecondary.x)
|
|
||||||
} else {
|
|
||||||
return .continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: BackgroundableViewController {
|
extension MainSplitViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -285,16 +285,6 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
func presentPreferences(completion: (() -> Void)?) {
|
||||||
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
guard presentedViewController == nil else {
|
|
||||||
return .stop
|
|
||||||
}
|
|
||||||
guard let vc = viewController(for: selectedTab) as? StatusBarTappableViewController else {
|
|
||||||
return .continue
|
|
||||||
}
|
|
||||||
return vc.handleStatusBarTapped(xPosition: xPosition)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController: BackgroundableViewController {
|
extension MainTabBarViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol TuskerRootViewController: UIViewController, StatusBarTappableViewController {
|
protocol TuskerRootViewController: UIViewController {
|
||||||
func presentCompose()
|
func presentCompose()
|
||||||
func select(tab: MainTabBarViewController.Tab)
|
func select(tab: MainTabBarViewController.Tab)
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
||||||
|
|
|
@ -269,7 +269,7 @@ extension NotificationsTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsTableViewController: TuskerNavigationDelegate {
|
extension NotificationsTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsTableViewController: MenuActionProvider {
|
extension NotificationsTableViewController: MenuActionProvider {
|
||||||
|
@ -294,4 +294,14 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
|
for indexPath in indexPaths {
|
||||||
|
guard let group = dataSource.itemIdentifier(for: indexPath)?.group else { continue }
|
||||||
|
for notification in group.notifications {
|
||||||
|
guard let avatar = notification.account.avatar else { continue }
|
||||||
|
ImageCache.avatars.cancelWithoutCallback(avatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,6 @@ fileprivate let instanceCell = "instanceCell"
|
||||||
|
|
||||||
class InstanceSelectorTableViewController: UITableViewController {
|
class InstanceSelectorTableViewController: UITableViewController {
|
||||||
|
|
||||||
static var blocks: [NSRegularExpression] = {
|
|
||||||
guard let path = Bundle.main.path(forResource: "DomainBlocks", ofType: "plist"),
|
|
||||||
let array = NSArray(contentsOfFile: path) as? [String] else { return [] }
|
|
||||||
return array.compactMap { try? NSRegularExpression(pattern: $0, options: .caseInsensitive) }
|
|
||||||
}()
|
|
||||||
|
|
||||||
weak var delegate: InstanceSelectorTableViewControllerDelegate?
|
weak var delegate: InstanceSelectorTableViewControllerDelegate?
|
||||||
|
|
||||||
var dataSource: DataSource!
|
var dataSource: DataSource!
|
||||||
|
@ -106,7 +100,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
loadRecommendedInstances()
|
loadRecommendedInstances()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func parseURLComponents(input: String) -> URLComponents? {
|
private func parseURLComponents(input: String) -> URLComponents {
|
||||||
// we can't just use the URLComponents(string:) initializer, because when given just a domain (w/o protocol), it interprets it as the path
|
// we can't just use the URLComponents(string:) initializer, because when given just a domain (w/o protocol), it interprets it as the path
|
||||||
var input = input
|
var input = input
|
||||||
var components = URLComponents()
|
var components = URLComponents()
|
||||||
|
@ -131,24 +125,13 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
components.port = Int(parts.last!)
|
components.port = Int(parts.last!)
|
||||||
}
|
}
|
||||||
components.host = input
|
components.host = input
|
||||||
if Self.blocks.contains(where: { $0.numberOfMatches(in: input, range: NSRange(location: 0, length: input.utf16.count)) > 0 }) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return components
|
return components
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSpecificInstance(domain: String) {
|
private func updateSpecificInstance(domain: String) {
|
||||||
activityIndicator.startAnimating()
|
activityIndicator.startAnimating()
|
||||||
|
|
||||||
guard let components = parseURLComponents(input: domain) else {
|
let components = parseURLComponents(input: domain)
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
if snapshot.indexOfSection(.selected) != nil {
|
|
||||||
snapshot.deleteSections([.selected])
|
|
||||||
dataSource.apply(snapshot)
|
|
||||||
}
|
|
||||||
activityIndicator.stopAnimating()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let url = components.url!
|
let url = components.url!
|
||||||
|
|
||||||
let client = Client(baseURL: url, session: .appDefault)
|
let client = Client(baseURL: url, session: .appDefault)
|
||||||
|
|
|
@ -17,6 +17,12 @@ protocol OnboardingViewControllerDelegate {
|
||||||
|
|
||||||
class OnboardingViewController: UINavigationController {
|
class OnboardingViewController: UINavigationController {
|
||||||
|
|
||||||
|
static var blocks: [NSRegularExpression] = {
|
||||||
|
guard let path = Bundle.main.path(forResource: "DomainBlocks", ofType: "plist"),
|
||||||
|
let array = NSArray(contentsOfFile: path) as? [String] else { return [] }
|
||||||
|
return array.compactMap { try? NSRegularExpression(pattern: $0, options: .caseInsensitive) }
|
||||||
|
}()
|
||||||
|
|
||||||
var onboardingDelegate: OnboardingViewControllerDelegate?
|
var onboardingDelegate: OnboardingViewControllerDelegate?
|
||||||
|
|
||||||
var instanceSelector = InstanceSelectorTableViewController()
|
var instanceSelector = InstanceSelectorTableViewController()
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import CoreData
|
|
||||||
|
|
||||||
struct AdvancedPrefsView : View {
|
struct AdvancedPrefsView : View {
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
@ -15,7 +14,7 @@ struct AdvancedPrefsView : View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
formattingSection
|
formattingSection
|
||||||
errorReportingSection
|
automationSection
|
||||||
cachingSection
|
cachingSection
|
||||||
}
|
}
|
||||||
.listStyle(InsetGroupedListStyle())
|
.listStyle(InsetGroupedListStyle())
|
||||||
|
@ -23,17 +22,7 @@ struct AdvancedPrefsView : View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var formattingFooter: some View {
|
var formattingFooter: some View {
|
||||||
var s: AttributedString = "This option is only supported with Pleroma and some compatible Mastodon instances (such as Glitch or Hometown).\n"
|
Text("This option is only supported for Pleroma and Mastodon instances with formatting enabled. On all other instances, formatting symbols will remain in the unformatted plain text.").lineLimit(nil)
|
||||||
if let account = LocalData.shared.getMostRecentAccount() {
|
|
||||||
let mastodonController = MastodonController.getForAccount(account)
|
|
||||||
// shouldn't need to load the instance here, because loading it is kicked off my the scene delegate
|
|
||||||
if !mastodonController.instanceFeatures.probablySupportsMarkdown {
|
|
||||||
var warning = AttributedString("\(account.instanceURL.host!) does not appear to support formatting. Using formatting symbols may not have an effect.")
|
|
||||||
warning[AttributeScopes.SwiftUIAttributes.FontAttribute.self] = .caption.bold()
|
|
||||||
s += warning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Text(s).lineLimit(nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var formattingSection: some View {
|
var formattingSection: some View {
|
||||||
|
@ -47,18 +36,10 @@ struct AdvancedPrefsView : View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorReportingSection: some View {
|
var automationSection: some View {
|
||||||
Section {
|
Section(header: Text("Automation")) {
|
||||||
Toggle("Report Errors Automatically", isOn: $preferences.reportErrorsAutomatically)
|
NavigationLink(destination: SilentActionPrefs()) {
|
||||||
} footer: {
|
Text("Silent Action Permissions")
|
||||||
var privacyPolicy: AttributedString = "Privacy Policy"
|
|
||||||
let _ = privacyPolicy.link = URL(string: "https://vaccor.space/tusker#privacy")!
|
|
||||||
if preferences.reportErrorsAutomatically {
|
|
||||||
Text(AttributedString("App crashes and errors will be automatically reported to the developer. You may be prompted to add additional information.\n") + privacyPolicy)
|
|
||||||
.lineLimit(nil)
|
|
||||||
} else {
|
|
||||||
Text(AttributedString("Errors will not be reported automatically. When a crash occurs, you may be asked to report it manually.\n") + privacyPolicy)
|
|
||||||
.lineLimit(nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,16 +58,9 @@ struct AdvancedPrefsView : View {
|
||||||
private func clearCache() {
|
private func clearCache() {
|
||||||
for account in LocalData.shared.accounts {
|
for account in LocalData.shared.accounts {
|
||||||
let controller = MastodonController.getForAccount(account)
|
let controller = MastodonController.getForAccount(account)
|
||||||
let container = controller.persistentContainer
|
let coordinator = controller.persistentContainer.persistentStoreCoordinator
|
||||||
do {
|
for store in coordinator.persistentStores {
|
||||||
let statusesReq = NSBatchDeleteRequest(fetchRequest: StatusMO.fetchRequest())
|
try! coordinator.destroyPersistentStore(at: store.url!, ofType: store.type, options: store.options)
|
||||||
try container.viewContext.execute(statusesReq)
|
|
||||||
let accountsReq = NSBatchDeleteRequest(fetchRequest: AccountMO.fetchRequest())
|
|
||||||
try container.viewContext.execute(accountsReq)
|
|
||||||
let relationshipsReq = NSBatchDeleteRequest(fetchRequest: RelationshipMO.fetchRequest())
|
|
||||||
try container.viewContext.execute(relationshipsReq)
|
|
||||||
} catch {
|
|
||||||
Logging.general.error("Error while clearing Mastodon cache: \(String(describing: error), privacy: .public)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resetUI()
|
resetUI()
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
// SilentActionPrefs.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 6/13/19.
|
||||||
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SilentActionPrefs : View {
|
||||||
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List(Array(preferences.silentActions.keys), id: \.self) { source in
|
||||||
|
SilentActionPermissionCell(source: source)
|
||||||
|
}
|
||||||
|
.listStyle(InsetGroupedListStyle())
|
||||||
|
// .navigationBarTitle("Silent Action Permissions")
|
||||||
|
// see FB6838291
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SilentActionPermissionCell: View {
|
||||||
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
let source: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Toggle(isOn: Binding(get: {
|
||||||
|
self.preferences.silentActions[self.source] == .accepted
|
||||||
|
}, set: {
|
||||||
|
self.preferences.silentActions[self.source] = $0 ? .accepted : .rejected
|
||||||
|
})) {
|
||||||
|
Text(verbatim: source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
struct SilentActionPrefs_Previews : PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SilentActionPrefs().environmentObject(Preferences.shared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -1,70 +0,0 @@
|
||||||
//
|
|
||||||
// ProfileHeaderCollectionViewCell.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 10/10/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class ProfileHeaderCollectionViewCell: UICollectionViewCell {
|
|
||||||
|
|
||||||
private var state: State = .unloaded
|
|
||||||
|
|
||||||
var view: ProfileHeaderView? {
|
|
||||||
if case .view(let view) = state {
|
|
||||||
return view
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
contentView.backgroundColor = .systemBackground
|
|
||||||
isOpaque = true
|
|
||||||
contentView.isOpaque = true
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func addHeader(_ header: ProfileHeaderView) {
|
|
||||||
switch state {
|
|
||||||
case .unloaded, .placeholder(heightConstraint: _):
|
|
||||||
header.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.embedSubview(header)
|
|
||||||
self.state = .view(header)
|
|
||||||
case .view(_):
|
|
||||||
fatalError("profile header collection view cell already has view")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addConstraint(height: CGFloat) -> ProfileHeaderView? {
|
|
||||||
switch state {
|
|
||||||
case .unloaded:
|
|
||||||
let constraint = contentView.heightAnchor.constraint(equalToConstant: height)
|
|
||||||
constraint.isActive = true
|
|
||||||
state = .placeholder(heightConstraint: constraint)
|
|
||||||
return nil
|
|
||||||
case .placeholder(let heightConstraint):
|
|
||||||
heightConstraint.constant = height
|
|
||||||
return nil
|
|
||||||
case .view(let header):
|
|
||||||
let constraint = contentView.heightAnchor.constraint(equalToConstant: height)
|
|
||||||
constraint.isActive = true
|
|
||||||
state = .placeholder(heightConstraint: constraint)
|
|
||||||
return header
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum State {
|
|
||||||
case unloaded
|
|
||||||
case placeholder(heightConstraint: NSLayoutConstraint)
|
|
||||||
case view(ProfileHeaderView)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -2,487 +2,287 @@
|
||||||
// ProfileStatusesViewController.swift
|
// ProfileStatusesViewController.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 10/6/22.
|
// Created by Shadowfacts on 7/3/20.
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
|
||||||
|
|
||||||
class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionViewController {
|
class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<ProfileStatusesViewController.Section, ProfileStatusesViewController.Item> {
|
||||||
|
|
||||||
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
private(set) var headerView: ProfileHeaderView!
|
||||||
|
|
||||||
|
var accountID: String!
|
||||||
|
|
||||||
weak var owner: ProfileViewController?
|
|
||||||
let mastodonController: MastodonController
|
|
||||||
private(set) var accountID: String!
|
|
||||||
let kind: Kind
|
let kind: Kind
|
||||||
var initialHeaderMode: HeaderMode?
|
|
||||||
weak var profileHeaderDelegate: ProfileHeaderViewDelegate?
|
|
||||||
|
|
||||||
private(set) var controller: TimelineLikeController<TimelineItem>!
|
|
||||||
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
|
||||||
private var newer: RequestRange?
|
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var newer: RequestRange?
|
||||||
|
|
||||||
var collectionView: UICollectionView {
|
init(accountID: String?, kind: Kind, mastodonController: MastodonController) {
|
||||||
view as! UICollectionView
|
|
||||||
}
|
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
||||||
private(set) var headerCell: ProfileHeaderCollectionViewCell?
|
|
||||||
|
|
||||||
private var state: State = .unloaded
|
|
||||||
|
|
||||||
init(accountID: String?, kind: Kind, owner: ProfileViewController) {
|
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.owner = owner
|
self.mastodonController = mastodonController
|
||||||
self.mastodonController = owner.mastodonController
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init()
|
||||||
|
|
||||||
self.controller = TimelineLikeController(delegate: self)
|
dragEnabled = true
|
||||||
|
|
||||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Profile"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadView() {
|
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
|
||||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
|
||||||
}
|
|
||||||
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
|
||||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
|
||||||
}
|
|
||||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionSeparatorConfiguration in
|
|
||||||
guard let item = self.dataSource.itemIdentifier(for: indexPath) else {
|
|
||||||
return sectionSeparatorConfiguration
|
|
||||||
}
|
|
||||||
var config = sectionSeparatorConfiguration
|
|
||||||
if item.hideSeparators {
|
|
||||||
config.topSeparatorVisibility = .hidden
|
|
||||||
config.bottomSeparatorVisibility = .hidden
|
|
||||||
}
|
|
||||||
if case .status(_, _, _) = item {
|
|
||||||
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
|
||||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
|
||||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
||||||
collectionView.delegate = self
|
|
||||||
collectionView.dragDelegate = self
|
|
||||||
|
|
||||||
registerTimelineLikeCells()
|
|
||||||
dataSource = createDataSource()
|
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
collectionView.refreshControl = UIRefreshControl()
|
|
||||||
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
mastodonController.persistentContainer.accountSubject
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.filter { [unowned self] in $0 == self.accountID }
|
// setup the initial snapshot with the sections in the right order, so we don't have to worry about order later
|
||||||
.sink { [unowned self] id in
|
var snapshot = Snapshot()
|
||||||
switch state {
|
snapshot.appendSections([.pinned, .statuses])
|
||||||
case .unloaded:
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
Task {
|
|
||||||
await load()
|
|
||||||
}
|
|
||||||
case .loading:
|
|
||||||
break
|
|
||||||
case .loaded, .setupInitialSnapshot:
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.reconfigureItems([.header(id)])
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
func updateUI(account: AccountMO) {
|
||||||
collectionView.register(ProfileHeaderCollectionViewCell.self, forCellWithReuseIdentifier: "headerCell")
|
if isViewLoaded {
|
||||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, StatusState, Bool)> { [unowned self] cell, indexPath, item in
|
reloadInitial()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override class func refreshCommandTitle() -> String {
|
||||||
|
return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - DiffableTimelineLikeTableViewController
|
||||||
|
|
||||||
|
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
|
||||||
|
switch item {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return self.loadingIndicatorCell(indexPath: indexPath)
|
||||||
|
|
||||||
|
case let .status(id: id, state: state, pinned: pinned):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
cell.showPinned = item.2
|
cell.showPinned = pinned
|
||||||
cell.updateUI(statusID: item.0, state: item.1)
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
}
|
||||||
switch itemIdentifier {
|
|
||||||
case .header(let id):
|
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
if let headerCell = self.headerCell {
|
guard accountID != nil else {
|
||||||
return headerCell
|
completion(.failure(.noClient))
|
||||||
} else {
|
return
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "headerCell", for: indexPath) as! ProfileHeaderCollectionViewCell
|
}
|
||||||
switch self.initialHeaderMode {
|
|
||||||
case nil:
|
getStatuses { (response) in
|
||||||
fatalError("missing initialHeaderMode")
|
guard self.state == .loadingInitial else {
|
||||||
case .createView:
|
return
|
||||||
let view = ProfileHeaderView.create()
|
}
|
||||||
view.delegate = self.profileHeaderDelegate
|
|
||||||
view.updateUI(for: id)
|
switch response {
|
||||||
view.pagesSegmentedControl.selectedSegmentIndex = self.owner?.currentIndex ?? 0
|
case let .failure(error):
|
||||||
cell.addHeader(view)
|
completion(.failure(.client(error)))
|
||||||
case .placeholder(height: let height):
|
|
||||||
_ = cell.addConstraint(height: height)
|
case let .success(statuses, _):
|
||||||
}
|
if !statuses.isEmpty {
|
||||||
self.headerCell = cell
|
self.newer = .after(id: statuses.first!.id, count: nil)
|
||||||
return cell
|
self.older = .before(id: statuses.last!.id, count: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, pinned: false) }, toSection: .statuses)
|
||||||
|
if self.kind == .statuses {
|
||||||
|
self.loadPinnedStatuses(snapshot: { snapshot }, completion: completion)
|
||||||
|
} else {
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .status(id: let id, state: let state, pinned: let pinned):
|
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, pinned))
|
|
||||||
case .loadingIndicator:
|
|
||||||
return loadingIndicatorCell(for: indexPath)
|
|
||||||
case .confirmLoadMore:
|
|
||||||
return confirmLoadMoreCell(for: indexPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
private func loadPinnedStatuses(snapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
super.viewWillAppear(animated)
|
guard kind == .statuses,
|
||||||
|
|
||||||
collectionView.indexPathsForSelectedItems?.forEach {
|
|
||||||
collectionView.deselectItem(at: $0, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
Task {
|
|
||||||
if case .notLoadedInitial = await controller.state {
|
|
||||||
await load()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setAccountID(_ id: String) {
|
|
||||||
self.accountID = id
|
|
||||||
// TODO: maybe this function should be async?
|
|
||||||
Task {
|
|
||||||
await load()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func load() async {
|
|
||||||
guard isViewLoaded,
|
|
||||||
let accountID,
|
|
||||||
case .unloaded = state,
|
|
||||||
mastodonController.persistentContainer.account(for: accountID) != nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state = .loading
|
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
||||||
snapshot.appendSections([.header, .pinned, .statuses])
|
|
||||||
snapshot.appendItems([.header(accountID)], toSection: .header)
|
|
||||||
await apply(snapshot, animatingDifferences: false)
|
|
||||||
|
|
||||||
state = .setupInitialSnapshot
|
|
||||||
|
|
||||||
await controller.loadInitial()
|
|
||||||
await tryLoadPinned()
|
|
||||||
|
|
||||||
state = .loaded
|
|
||||||
}
|
|
||||||
|
|
||||||
private func tryLoadPinned() async {
|
|
||||||
do {
|
|
||||||
try await loadPinned()
|
|
||||||
} catch {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Loading Pinned", in: self) { toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
await self.tryLoadPinned()
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadPinned() async throws {
|
|
||||||
guard case .statuses = kind,
|
|
||||||
mastodonController.instanceFeatures.profilePinnedStatuses else {
|
mastodonController.instanceFeatures.profilePinnedStatuses else {
|
||||||
|
completion(.success(snapshot()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
getPinnedStatuses { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
let request = Account.getStatuses(accountID, range: .default, onlyMedia: false, pinned: true, excludeReplies: false)
|
case let .success(statuses, _):
|
||||||
let (statuses, _) = try await mastodonController.run(request)
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
await withCheckedContinuation { continuation in
|
var snapshot = snapshot()
|
||||||
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .pinned))
|
||||||
continuation.resume()
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, pinned: true) }, toSection: .pinned)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
let items = statuses.map { Item.status(id: $0.id, state: .unknown, pinned: true) }
|
|
||||||
snapshot.appendItems(items, toSection: .pinned)
|
|
||||||
await apply(snapshot, animatingDifferences: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func refresh() {
|
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard case .loaded = state else {
|
guard let older = older else {
|
||||||
#if !targetEnvironment(macCatalyst)
|
completion(.failure(.noOlder))
|
||||||
collectionView.refreshControl?.endRefreshing()
|
|
||||||
#endif
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Task {
|
|
||||||
// TODO: coalesce these data source updates
|
getStatuses(for: older) { (response) in
|
||||||
// TODO: refresh profile
|
switch response {
|
||||||
await controller.loadNewer()
|
case let .failure(error):
|
||||||
await tryLoadPinned()
|
completion(.failure(.client(error)))
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
collectionView.refreshControl?.endRefreshing()
|
case let .success(statuses, _):
|
||||||
#endif
|
guard !statuses.isEmpty else {
|
||||||
|
completion(.failure(.noOlder))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.older = .before(id: statuses.last!.id, count: nil)
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
var snapshot = currentSnapshot()
|
||||||
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, pinned: false) }, toSection: .statuses)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController {
|
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
enum State {
|
guard let newer = newer else {
|
||||||
case unloaded
|
completion(.failure(.noNewer))
|
||||||
case loading
|
return
|
||||||
case setupInitialSnapshot
|
}
|
||||||
case loaded
|
|
||||||
|
getStatuses(for: newer) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(statuses, _):
|
||||||
|
guard !statuses.isEmpty else {
|
||||||
|
completion(.failure(.allCaughtUp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.newer = .after(id: statuses.first!.id, count: nil)
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
var snapshot = currentSnapshot()
|
||||||
|
let items = statuses.map { Item.status(id: $0.id, state: .unknown, pinned: false) }
|
||||||
|
if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
|
||||||
|
snapshot.insertItems(items, beforeItem: first)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems(items, toSection: .statuses)
|
||||||
|
}
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getStatuses(for range: RequestRange = .default, completion: @escaping Client.Callback<[Status]>) {
|
||||||
|
let request: Request<[Status]>
|
||||||
|
switch kind {
|
||||||
|
case .statuses:
|
||||||
|
request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: true)
|
||||||
|
case .withReplies:
|
||||||
|
request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: false)
|
||||||
|
case .onlyMedia:
|
||||||
|
request = Account.getStatuses(accountID, range: range, onlyMedia: true, pinned: false, excludeReplies: false)
|
||||||
|
}
|
||||||
|
mastodonController.run(request, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getPinnedStatuses(completion: @escaping Client.Callback<[Status]>) {
|
||||||
|
let request = Account.getStatuses(accountID, range: .default, onlyMedia: false, pinned: true, excludeReplies: false)
|
||||||
|
mastodonController.run(request, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func refresh() {
|
||||||
|
super.refresh()
|
||||||
|
|
||||||
|
// only refresh pinned if the super call actually succeded (put the state into .loadingNewer)
|
||||||
|
if state == .loadingNewer,
|
||||||
|
kind == .statuses {
|
||||||
|
loadPinnedStatuses(snapshot: dataSource.snapshot) { (result) in
|
||||||
|
switch result {
|
||||||
|
case .failure(_):
|
||||||
|
break
|
||||||
|
|
||||||
|
case let .success(snapshot):
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController {
|
extension ProfileStatusesViewController {
|
||||||
enum Kind {
|
enum Kind {
|
||||||
case statuses, withReplies, onlyMedia
|
case statuses, withReplies, onlyMedia
|
||||||
}
|
}
|
||||||
enum HeaderMode {
|
|
||||||
case createView, placeholder(height: CGFloat)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController {
|
extension ProfileStatusesViewController {
|
||||||
enum Section: TimelineLikeCollectionViewSection {
|
enum Section: DiffableTimelineLikeSection {
|
||||||
case header
|
case loadingIndicator
|
||||||
case pinned
|
case pinned
|
||||||
case statuses
|
case statuses
|
||||||
case footer
|
|
||||||
|
|
||||||
static var entries: Self { .statuses }
|
|
||||||
}
|
}
|
||||||
enum Item: TimelineLikeCollectionViewItem {
|
enum Item: DiffableTimelineLikeItem {
|
||||||
typealias TimelineItem = String
|
|
||||||
|
|
||||||
case header(String)
|
|
||||||
case status(id: String, state: StatusState, pinned: Bool)
|
|
||||||
case loadingIndicator
|
case loadingIndicator
|
||||||
case confirmLoadMore
|
case status(id: String, state: StatusState, pinned: Bool)
|
||||||
|
|
||||||
static func fromTimelineItem(_ item: String) -> Self {
|
var id: String? {
|
||||||
return .status(id: item, state: .unknown, pinned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
|
||||||
switch (lhs, rhs) {
|
|
||||||
case let (.header(a), .header(b)):
|
|
||||||
return a == b
|
|
||||||
case let (.status(id: a, state: _, pinned: ap), .status(id: b, state: _, pinned: bp)):
|
|
||||||
return a == b && ap == bp
|
|
||||||
case (.loadingIndicator, .loadingIndicator):
|
|
||||||
return true
|
|
||||||
case (.confirmLoadMore, .confirmLoadMore):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
switch self {
|
switch self {
|
||||||
case .header(let id):
|
|
||||||
hasher.combine(0)
|
|
||||||
hasher.combine(id)
|
|
||||||
case .status(id: let id, state: _, pinned: let pinned):
|
|
||||||
hasher.combine(1)
|
|
||||||
hasher.combine(id)
|
|
||||||
hasher.combine(pinned)
|
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
hasher.combine(2)
|
return nil
|
||||||
case .confirmLoadMore:
|
case .status(id: let id, state: _, pinned: _):
|
||||||
hasher.combine(3)
|
return id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hideSeparators: Bool {
|
|
||||||
switch self {
|
|
||||||
case .loadingIndicator, .confirmLoadMore:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSelectable: Bool {
|
|
||||||
switch self {
|
|
||||||
case .status(id: _, state: _, pinned: _):
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController: TimelineLikeControllerDelegate {
|
|
||||||
typealias TimelineItem = String // status ID
|
|
||||||
|
|
||||||
private func request(for range: RequestRange = .default) -> Request<[Status]> {
|
|
||||||
switch kind {
|
|
||||||
case .statuses:
|
|
||||||
return Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: true)
|
|
||||||
case .withReplies:
|
|
||||||
return Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: false)
|
|
||||||
case .onlyMedia:
|
|
||||||
return Account.getStatuses(accountID, range: range, onlyMedia: true, pinned: false, excludeReplies: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadInitial() async throws -> [String] {
|
|
||||||
let request = request()
|
|
||||||
let (statuses, _) = try await mastodonController.run(request)
|
|
||||||
|
|
||||||
if !statuses.isEmpty {
|
|
||||||
newer = .after(id: statuses.first!.id, count: nil)
|
|
||||||
older = .before(id: statuses.last!.id, count: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return await withCheckedContinuation { continuation in
|
|
||||||
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
continuation.resume(returning: statuses.map(\.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadNewer() async throws -> [String] {
|
|
||||||
guard let newer else {
|
|
||||||
throw Error.noNewer
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = request(for: newer)
|
|
||||||
let (statuses, _) = try await mastodonController.run(request)
|
|
||||||
|
|
||||||
guard !statuses.isEmpty else {
|
|
||||||
throw Error.allCaughtUp
|
|
||||||
}
|
|
||||||
|
|
||||||
self.newer = .after(id: statuses.first!.id, count: nil)
|
|
||||||
|
|
||||||
return await withCheckedContinuation { continuation in
|
|
||||||
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
continuation.resume(returning: statuses.map(\.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadOlder() async throws -> [String] {
|
|
||||||
guard let older else {
|
|
||||||
throw Error.noOlder
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = request(for: older)
|
|
||||||
let (statuses, _) = try await mastodonController.run(request)
|
|
||||||
|
|
||||||
guard !statuses.isEmpty else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
self.older = .before(id: statuses.last!.id, count: nil)
|
|
||||||
|
|
||||||
return await withCheckedContinuation { continuation in
|
|
||||||
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
continuation.resume(returning: statuses.map(\.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Error: TimelineLikeCollectionViewError {
|
|
||||||
case noNewer
|
|
||||||
case noOlder
|
|
||||||
case allCaughtUp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController: UICollectionViewDelegate {
|
|
||||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
|
||||||
guard case .statuses = dataSource.sectionIdentifier(for: indexPath.section) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let itemsInSection = collectionView.numberOfItems(inSection: indexPath.section)
|
|
||||||
if indexPath.row == itemsInSection - 1 {
|
|
||||||
Task {
|
|
||||||
await controller.loadOlder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
|
||||||
return dataSource.itemIdentifier(for: indexPath)?.isSelectable ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
||||||
guard case .status(id: let id, state: let state, pinned: _) = dataSource.itemIdentifier(for: indexPath) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let status = mastodonController.persistentContainer.status(for: id)!
|
|
||||||
selected(status: status.reblog?.id ?? id, state: state.copy())
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
|
||||||
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController: UICollectionViewDragDelegate {
|
|
||||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
|
||||||
(collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController: TuskerNavigationDelegate {
|
extension ProfileStatusesViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController: MenuActionProvider {
|
extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
||||||
}
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
extension ProfileStatusesViewController: StatusCollectionViewCellDelegate {
|
} else {
|
||||||
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
cellHeightChanged()
|
||||||
if let indexPath = collectionView.indexPath(for: cell) {
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController: TabBarScrollableViewController {
|
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tabBarScrollToTop() {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
collectionView.scrollToTop()
|
let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id }
|
||||||
|
prefetchStatuses(with: ids)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController: StatusBarTappableViewController {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id }
|
||||||
collectionView.scrollToTop()
|
cancelPrefetchingStatuses(with: ids)
|
||||||
return .stop
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
// ProfileViewController.swift
|
// ProfileViewController.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 10/10/22.
|
// Created by Shadowfacts on 7/3/20.
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
@ -18,43 +18,39 @@ class ProfileViewController: UIPageViewController {
|
||||||
// when first constructed. It should never be set to nil.
|
// when first constructed. It should never be set to nil.
|
||||||
var accountID: String? {
|
var accountID: String? {
|
||||||
willSet {
|
willSet {
|
||||||
precondition(newValue != nil, "Do not set ProfileViewController.accountID to nil")
|
if newValue == nil {
|
||||||
|
fatalError("Do not set ProfileViewController.accountID to nil")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
didSet {
|
didSet {
|
||||||
pageControllers.forEach { $0.setAccountID(accountID!) }
|
pageControllers.forEach { $0.accountID = accountID }
|
||||||
Task {
|
loadAccount()
|
||||||
await loadAccount()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var accountUpdater: Cancellable?
|
||||||
|
|
||||||
private(set) var currentIndex: Int!
|
private(set) var currentIndex: Int!
|
||||||
private var pageControllers: [ProfileStatusesViewController]!
|
let pageControllers: [ProfileStatusesViewController]
|
||||||
var currentViewController: ProfileStatusesViewController {
|
var currentViewController: ProfileStatusesViewController {
|
||||||
pageControllers[currentIndex]
|
pageControllers[currentIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
private var state: State = .idle
|
private var headerView: ProfileHeaderView!
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var hasAppeared = false
|
||||||
|
|
||||||
init(accountID: String?, mastodonController: MastodonController) {
|
init(accountID: String?, mastodonController: MastodonController) {
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
|
||||||
|
|
||||||
self.pageControllers = [
|
self.pageControllers = [
|
||||||
.init(accountID: accountID, kind: .statuses, owner: self),
|
ProfileStatusesViewController(accountID: accountID, kind: .statuses, mastodonController: mastodonController),
|
||||||
.init(accountID: accountID, kind: .withReplies, owner: self),
|
ProfileStatusesViewController(accountID: accountID, kind: .withReplies, mastodonController: mastodonController),
|
||||||
.init(accountID: accountID, kind: .onlyMedia, owner: self),
|
ProfileStatusesViewController(accountID: accountID, kind: .onlyMedia, mastodonController: mastodonController)
|
||||||
]
|
]
|
||||||
|
|
||||||
// try to update the account UI immediately if possible, to avoid the navigation title popping in later
|
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||||
if let accountID,
|
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID) {
|
|
||||||
updateAccountUI(account: account)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -66,36 +62,35 @@ class ProfileViewController: UIPageViewController {
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
for pageController in pageControllers {
|
|
||||||
pageController.profileHeaderDelegate = self
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPage(at: 0, animated: false)
|
|
||||||
|
|
||||||
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
||||||
composeButton.menu = UIMenu(children: [
|
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
||||||
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), handler: { [unowned self] _ 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
|
composeButton.isEnabled = mastodonController.loggedIn
|
||||||
navigationItem.rightBarButtonItem = composeButton
|
navigationItem.rightBarButtonItem = composeButton
|
||||||
|
|
||||||
|
headerView = ProfileHeaderView.create()
|
||||||
|
headerView.delegate = self
|
||||||
|
|
||||||
|
selectPage(at: 0, animated: false)
|
||||||
|
|
||||||
|
currentViewController.tableView.tableHeaderView = headerView
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
headerView.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
addKeyCommand(MenuController.prevSubTabCommand)
|
addKeyCommand(MenuController.prevSubTabCommand)
|
||||||
addKeyCommand(MenuController.nextSubTabCommand)
|
addKeyCommand(MenuController.nextSubTabCommand)
|
||||||
|
|
||||||
mastodonController.persistentContainer.accountSubject
|
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.filter { [unowned self] in $0 == self.accountID }
|
.filter { [weak self] in $0 == self?.accountID }
|
||||||
.sink { [unowned self] id in
|
.sink { [weak self] (_) in self?.updateAccountUI() }
|
||||||
let account = self.mastodonController.persistentContainer.account(for: id)!
|
|
||||||
self.updateAccountUI(account: account)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
Task {
|
loadAccount()
|
||||||
await loadAccount()
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
|
// disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
|
||||||
if let nav = navigationController {
|
if let nav = navigationController {
|
||||||
|
@ -105,217 +100,182 @@ class ProfileViewController: UIPageViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadAccount() async {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
guard let accountID else {
|
super.viewDidAppear(animated)
|
||||||
return
|
|
||||||
}
|
hasAppeared = true
|
||||||
if let account = mastodonController.persistentContainer.account(for: accountID) {
|
}
|
||||||
updateAccountUI(account: account)
|
|
||||||
|
private func loadAccount() {
|
||||||
|
guard let accountID = accountID else { return }
|
||||||
|
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
||||||
|
updateAccountUI()
|
||||||
} else {
|
} else {
|
||||||
do {
|
let req = Client.getAccount(id: accountID)
|
||||||
let req = Client.getAccount(id: accountID)
|
mastodonController.run(req) { [weak self] (response) in
|
||||||
let (account, _) = try await mastodonController.run(req)
|
guard let self = self else { return }
|
||||||
let mo = await withCheckedContinuation { continuation in
|
switch response {
|
||||||
mastodonController.persistentContainer.addOrUpdate(account: account, in: mastodonController.persistentContainer.viewContext) { (mo) in
|
case .success(let account, _):
|
||||||
continuation.resume(returning: mo)
|
self.mastodonController.persistentContainer.addOrUpdate(account: account) { (account) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.updateAccountUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let config = ToastConfiguration(from: error, with: "Loading", in: self) { [unowned self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self.loadAccount()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.updateAccountUI(account: mo)
|
|
||||||
} catch {
|
|
||||||
let config = ToastConfiguration(from: error, with: "Loading Account", in: self) { [unowned self] toast in
|
|
||||||
toast.dismissToast(animated: true)
|
|
||||||
await self.loadAccount()
|
|
||||||
}
|
|
||||||
self.showToast(configuration: config, animated: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateAccountUI(account: AccountMO) {
|
private func updateAccountUI() {
|
||||||
if let currentAccountID = mastodonController.accountInfo?.id {
|
guard let accountID = accountID,
|
||||||
userActivity = UserActivityManager.showProfileActivity(id: account.id, accountID: currentAccountID)
|
let account = mastodonController.persistentContainer.account(for: accountID) else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let currentAccountID = mastodonController.accountInfo?.id {
|
||||||
|
userActivity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally invoke updateUI on headerView because viewDidLoad may not have been called yet
|
||||||
|
headerView?.updateUI(for: accountID)
|
||||||
navigationItem.title = account.displayNameWithoutCustomEmoji
|
navigationItem.title = account.displayNameWithoutCustomEmoji
|
||||||
|
|
||||||
|
// Only call updateUI on the individual page controllers if the account is loaded after the profile VC has appeared on screen.
|
||||||
|
// Otherwise, fi the page view controllers do something with the table view before they appear, the table view doesn't load
|
||||||
|
// its cells until the user begins to scroll.
|
||||||
|
if hasAppeared {
|
||||||
|
pageControllers.forEach {
|
||||||
|
$0.updateUI(account: account)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectPage(at index: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) {
|
private func selectPage(at index: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) {
|
||||||
guard case .idle = state else {
|
let direction: UIPageViewController.NavigationDirection = currentIndex == nil || index - currentIndex > 0 ? .forward : .reverse
|
||||||
return
|
currentIndex = index
|
||||||
}
|
|
||||||
|
|
||||||
state = .animating
|
headerView.pagesSegmentedControl.selectedSegmentIndex = index
|
||||||
|
|
||||||
let direction: UIPageViewController.NavigationDirection
|
|
||||||
if currentIndex == nil || index - currentIndex > 0 {
|
|
||||||
direction = .forward
|
|
||||||
} else {
|
|
||||||
direction = .reverse
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let old = viewControllers?.first as? ProfileStatusesViewController else {
|
guard let old = viewControllers?.first as? ProfileStatusesViewController else {
|
||||||
// if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
|
// if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
|
||||||
pageControllers[index].initialHeaderMode = .createView
|
// since it will be added in viewDidLoad
|
||||||
setViewControllers([pageControllers[index]], direction: direction, animated: animated) { finished in
|
setViewControllers([pageControllers[index]], direction: direction, animated: animated, completion: completion)
|
||||||
self.state = .idle
|
|
||||||
completion?(finished)
|
|
||||||
}
|
|
||||||
currentIndex = index
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let new = pageControllers[index]
|
let new = pageControllers[index]
|
||||||
|
|
||||||
currentIndex = index
|
let headerHeight = self.headerView.bounds.height
|
||||||
|
|
||||||
// TODO: old.headerCell could be nil if scrolled down and key command used
|
// Store old's content offset so it can be transferred to new
|
||||||
let oldHeaderCell = old.headerCell!
|
let prevOldContentOffset = old.tableView.contentOffset
|
||||||
|
// Remove the header, inset the table content by the same amount, and adjust the offset so the cells don't move
|
||||||
|
old.tableView.tableHeaderView = nil
|
||||||
|
old.tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0)
|
||||||
|
old.tableView.contentOffset.y -= headerHeight
|
||||||
|
|
||||||
// old header cell must have the header view
|
// Add the header to ourself temporarily, and constrain it to the same position it was in
|
||||||
let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)!
|
self.view.addSubview(self.headerView)
|
||||||
|
let tempTopConstraint = self.headerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -(prevOldContentOffset.y + old.tableView.safeAreaInsets.top))
|
||||||
if new.isViewLoaded {
|
|
||||||
_ = new.headerCell!.addConstraint(height: oldHeaderCell.bounds.height)
|
|
||||||
} else {
|
|
||||||
new.initialHeaderMode = .placeholder(height: oldHeaderCell.bounds.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable user interaction during animation, to avoid any potential weird race conditions
|
|
||||||
headerView.isUserInteractionEnabled = false
|
|
||||||
headerView.layer.zPosition = 100
|
|
||||||
view.addSubview(headerView)
|
|
||||||
let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y
|
|
||||||
// TODO: use safe area layout guide instead of manually adjusting this?
|
|
||||||
let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset),
|
self.headerView.widthAnchor.constraint(equalTo: self.view.widthAnchor),
|
||||||
headerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
tempTopConstraint
|
||||||
headerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// hide scroll indicators during the transition because otherwise the show through the
|
// Setup the inset in new, in case it hasn't been already
|
||||||
// profile header, even though it has an opaque background
|
new.tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0)
|
||||||
old.collectionView.showsVerticalScrollIndicator = false
|
// Match the scroll positions
|
||||||
if new.isViewLoaded {
|
new.tableView.contentOffset = old.tableView.contentOffset
|
||||||
new.collectionView.showsVerticalScrollIndicator = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the new view isn't loaded or it isn't tall enough to match content offsets, animate scrolling old back to top to match new
|
// Actually switch pages
|
||||||
if animated,
|
setViewControllers([pageControllers[index]], direction: direction, animated: animated) { (finished) in
|
||||||
!new.isViewLoaded || new.collectionView.contentSize.height - new.collectionView.bounds.height < old.collectionView.contentOffset.y {
|
// Defer everything one run-loop iteration, otherwise altering the tableView's contentInset/Offset causes it to jump around during the animation
|
||||||
// We need to display a snapshot over the old view because setting the content offset to the top w/o animating
|
DispatchQueue.main.async {
|
||||||
// results in the collection view immediately removing cells that will be offscreen.
|
// Move the header to the new table view
|
||||||
// And we can't just call setContentOffset(_:animated:) because its animation curve does not match ours/the page views
|
new.tableView.tableHeaderView = self.headerView
|
||||||
// So, we capture a snapshot before the content offset is changed, so those cells can be shown during the animation,
|
// Remove the inset, and set the offset back to old's original one, prior to removing the header
|
||||||
// rather than a gap appearing during it.
|
new.tableView.contentInset = .zero
|
||||||
let snapshot = old.collectionView.snapshotView(afterScreenUpdates: true)!
|
new.tableView.contentOffset = prevOldContentOffset
|
||||||
let origOldContentOffset = old.collectionView.contentOffset
|
|
||||||
old.collectionView.contentOffset = CGPoint(x: 0, y: view.safeAreaInsets.top)
|
|
||||||
|
|
||||||
snapshot.frame = old.collectionView.bounds
|
// Deactivate the top constraint, otherwise it sticks around
|
||||||
snapshot.frame.origin.y = 0
|
tempTopConstraint.isActive = false
|
||||||
snapshot.layer.zPosition = 99
|
// Re-add the width constraint since it was removed by re-parenting the view
|
||||||
view.addSubview(snapshot)
|
// Why was the width constraint removed, but the top one not? Good question, I have no idea.
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
self.headerView.widthAnchor.constraint(equalTo: self.view.widthAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
// empirically, 0.3s seems to match the UIPageViewController animation
|
// Layout and update the table view, otherwise the content jumps around when first scrolling it,
|
||||||
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
|
// if old was not scrolled all the way to the top
|
||||||
// animate the snapshot offscreen in the same direction as the old view
|
new.tableView.layoutIfNeeded()
|
||||||
snapshot.frame.origin.x = direction == .forward ? -self.view.bounds.width : self.view.bounds.width
|
let snapshot = new.dataSource.snapshot()
|
||||||
// animate the snapshot to be "scrolled" to top
|
new.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
snapshot.frame.origin.y = self.view.safeAreaInsets.top + origOldContentOffset.y
|
|
||||||
// if scrolling because the new collection view's content isn't tall enough, make sure to scroll it to top as well
|
completion?(finished)
|
||||||
if new.isViewLoaded {
|
|
||||||
new.collectionView.contentOffset = CGPoint(x: 0, y: -self.view.safeAreaInsets.top)
|
|
||||||
}
|
|
||||||
headerView.transform = CGAffineTransform(translationX: 0, y: -headerTopOffset)
|
|
||||||
} completion: { _ in
|
|
||||||
snapshot.removeFromSuperview()
|
|
||||||
}
|
}
|
||||||
} else if new.isViewLoaded {
|
|
||||||
new.collectionView.contentOffset = old.collectionView.contentOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewControllers([pageControllers[index]], direction: direction, animated: animated) { finished in
|
|
||||||
// reenable scroll indicators after the switching animation is done
|
|
||||||
old.collectionView.showsVerticalScrollIndicator = true
|
|
||||||
new.collectionView.showsVerticalScrollIndicator = true
|
|
||||||
|
|
||||||
headerView.isUserInteractionEnabled = true
|
|
||||||
|
|
||||||
headerView.transform = .identity
|
|
||||||
headerView.layer.zPosition = 0
|
|
||||||
// move the header view into the new page controller's cell
|
|
||||||
// new's headerCell should always be non-nil, because the account must be loaded (in order to have triggered this switch), and so new should add the cell immediately on load
|
|
||||||
new.headerCell!.addHeader(headerView)
|
|
||||||
|
|
||||||
self.state = .idle
|
|
||||||
completion?(finished)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Interaction
|
// MARK: Interaction
|
||||||
|
|
||||||
@objc private func composeMentioning() {
|
@objc private func composeMentioning() {
|
||||||
if let accountID,
|
if let accountID = accountID,
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID) {
|
let account = mastodonController.persistentContainer.account(for: accountID) {
|
||||||
compose(mentioningAcct: account.acct)
|
compose(mentioningAcct: account.acct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func composeDirectMentioning() {
|
private func composeDirectMentioning() {
|
||||||
if let accountID,
|
if let accountID = accountID,
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID) {
|
let account = mastodonController.persistentContainer.account(for: accountID) {
|
||||||
let draft = mastodonController.createDraft(mentioningAcct: account.acct)
|
let draft = mastodonController.createDraft(mentioningAcct: account.acct)
|
||||||
draft.visibility = .direct
|
draft.visibility = .direct
|
||||||
compose(editing: draft)
|
compose(editing: draft)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileViewController {
|
|
||||||
enum State {
|
|
||||||
case idle
|
|
||||||
case animating
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController: TuskerNavigationDelegate {
|
extension ProfileViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileViewController: ToastableViewController {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController: ProfileHeaderViewDelegate {
|
extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||||
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) {
|
func profileHeader(_ view: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) {
|
||||||
guard case .idle = state else {
|
// disable user interaction on segmented control while switching pages to prevent
|
||||||
return
|
// race condition from trying to switch to multiple pages simultaneously
|
||||||
|
view.pagesSegmentedControl.isUserInteractionEnabled = false
|
||||||
|
selectPage(at: newIndex, animated: true) { (finished) in
|
||||||
|
view.pagesSegmentedControl.isUserInteractionEnabled = true
|
||||||
}
|
}
|
||||||
selectPage(at: newIndex, animated: true)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileViewController: TabBarScrollableViewController {
|
||||||
|
func tabBarScrollToTop() {
|
||||||
|
pageControllers[currentIndex].tabBarScrollToTop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController: TabbedPageViewController {
|
extension ProfileViewController: TabbedPageViewController {
|
||||||
func selectNextPage() {
|
func selectNextPage() {
|
||||||
guard currentIndex < pageControllers.count - 1 else { return }
|
guard currentIndex < pageControllers.count - 1 else { return }
|
||||||
currentViewController.headerCell?.view?.pagesSegmentedControl.selectedSegmentIndex = currentIndex + 1
|
|
||||||
selectPage(at: currentIndex + 1, animated: true)
|
selectPage(at: currentIndex + 1, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectPrevPage() {
|
func selectPrevPage() {
|
||||||
guard currentIndex > 0 else { return }
|
guard currentIndex > 0 else { return }
|
||||||
currentViewController.headerCell?.view?.pagesSegmentedControl.selectedSegmentIndex = currentIndex - 1
|
|
||||||
selectPage(at: currentIndex - 1, animated: true)
|
selectPage(at: currentIndex - 1, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController: TabBarScrollableViewController {
|
extension ProfileViewController: ToastableViewController {
|
||||||
func tabBarScrollToTop() {
|
|
||||||
currentViewController.tabBarScrollToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileViewController: StatusBarTappableViewController {
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
return currentViewController.handleStatusBarTapped(xPosition: xPosition)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,7 +289,6 @@ extension SearchResultsViewController: UISearchBarDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultsViewController: TuskerNavigationDelegate {
|
extension SearchResultsViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultsViewController: ToastableViewController {
|
extension SearchResultsViewController: ToastableViewController {
|
||||||
|
@ -299,6 +298,7 @@ extension SearchResultsViewController: MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchResultsViewController: StatusTableViewCellDelegate {
|
extension SearchResultsViewController: StatusTableViewCellDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
tableView.beginUpdates()
|
tableView.beginUpdates()
|
||||||
tableView.endUpdates()
|
tableView.endUpdates()
|
||||||
|
|
|
@ -319,7 +319,7 @@ extension SearchViewController: UICollectionViewDragDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchViewController: TuskerNavigationDelegate {
|
extension SearchViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchViewController: ToastableViewController {
|
extension SearchViewController: ToastableViewController {
|
||||||
|
|
|
@ -145,7 +145,6 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusActionAccountListTableViewController: TuskerNavigationDelegate {
|
extension StatusActionAccountListTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusActionAccountListTableViewController: ToastableViewController {
|
extension StatusActionAccountListTableViewController: ToastableViewController {
|
||||||
|
@ -155,6 +154,7 @@ extension StatusActionAccountListTableViewController: MenuActionProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate {
|
extension StatusActionAccountListTableViewController: StatusTableViewCellDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
// causes the table view to recalculate the cell heights
|
// causes the table view to recalculate the cell heights
|
||||||
tableView.beginUpdates()
|
tableView.beginUpdates()
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class HashtagTimelineViewController: TimelineViewController {
|
class HashtagTimelineViewController: TimelineTableViewController {
|
||||||
|
|
||||||
let hashtag: Hashtag
|
let hashtag: Hashtag
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,13 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
protocol InstanceTimelineViewControllerDelegate: AnyObject {
|
protocol InstanceTimelineViewControllerDelegate: AnyObject {
|
||||||
func didSaveInstance(url: URL)
|
func didSaveInstance(url: URL)
|
||||||
func didUnsaveInstance(url: URL)
|
func didUnsaveInstance(url: URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
class InstanceTimelineViewController: TimelineViewController {
|
class InstanceTimelineViewController: TimelineTableViewController {
|
||||||
|
|
||||||
weak var delegate: InstanceTimelineViewControllerDelegate?
|
weak var delegate: InstanceTimelineViewControllerDelegate?
|
||||||
|
|
||||||
|
@ -69,15 +68,19 @@ class InstanceTimelineViewController: TimelineViewController {
|
||||||
toggleSaveButton.title = toggleSaveButtonTitle
|
toggleSaveButton.title = toggleSaveButtonTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
override func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: StatusState) {
|
// MARK: - Table view data source
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell = super.tableView(tableView, cellForRowAt: indexPath) as! TimelineStatusTableViewCell
|
||||||
cell.delegate = browsingEnabled ? self : nil
|
cell.delegate = browsingEnabled ? self : nil
|
||||||
cell.overrideMastodonController = mastodonController
|
return cell
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
guard browsingEnabled else { return }
|
guard browsingEnabled else { return }
|
||||||
super.collectionView(collectionView, didSelectItemAt: indexPath)
|
super.tableView(tableView, didSelectRowAt: indexPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
|
@ -0,0 +1,334 @@
|
||||||
|
//
|
||||||
|
// TimelineTableViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/15/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
typealias TimelineEntry = (id: String, state: StatusState)
|
||||||
|
|
||||||
|
class TimelineTableViewController: DiffableTimelineLikeTableViewController<TimelineTableViewController.Section, TimelineTableViewController.Item> {
|
||||||
|
|
||||||
|
let timeline: Timeline
|
||||||
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
private var newer: RequestRange?
|
||||||
|
private var older: RequestRange?
|
||||||
|
|
||||||
|
private var didConfirmLoadMore = false
|
||||||
|
private var isShowingTimelineDescription = false
|
||||||
|
|
||||||
|
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||||
|
self.timeline = timeline
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
dragEnabled = true
|
||||||
|
|
||||||
|
title = timeline.title
|
||||||
|
tabBarItem.image = timeline.tabBarImage
|
||||||
|
|
||||||
|
if let id = mastodonController.accountInfo?.id {
|
||||||
|
userActivity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return NSLocalizedString("Refresh Statuses", comment: "refresh status command discoverability title")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func timelineContentSections() -> [Section] {
|
||||||
|
return [.statuses]
|
||||||
|
}
|
||||||
|
|
||||||
|
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
|
||||||
|
switch item {
|
||||||
|
case .loadingIndicator:
|
||||||
|
return self.loadingIndicatorCell(indexPath: indexPath)
|
||||||
|
|
||||||
|
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 (LoadResult) -> Void) {
|
||||||
|
guard let mastodonController = mastodonController else {
|
||||||
|
completion(.failure(.noClient))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Client.getStatuses(timeline: timeline)
|
||||||
|
|
||||||
|
mastodonController.run(request) { response in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(statuses, _):
|
||||||
|
if !statuses.isEmpty {
|
||||||
|
self.newer = .after(id: statuses.first!.id, count: nil)
|
||||||
|
self.older = .before(id: statuses.last!.id, count: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
if snapshot.sectionIdentifiers.contains(.loadingIndicator) {
|
||||||
|
snapshot.deleteSections([.loadingIndicator])
|
||||||
|
}
|
||||||
|
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 loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
|
guard let older = older else {
|
||||||
|
completion(.failure(.noOlder))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if 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)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||||
|
|
||||||
|
mastodonController.run(request) { response in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(statuses, _):
|
||||||
|
if !statuses.isEmpty {
|
||||||
|
self.older = .before(id: statuses.last!.id, count: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
var snapshot = currentSnapshot()
|
||||||
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
|
snapshot.deleteItems([.confirmLoadMore])
|
||||||
|
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.getStatuses(timeline: timeline, range: newer)
|
||||||
|
mastodonController.run(request) { response in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(statuses, _):
|
||||||
|
guard !statuses.isEmpty else {
|
||||||
|
completion(.failure(.allCaughtUp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.newer = .after(id: statuses.first!.id, count: nil)
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: DiffableTimelineLikeSection {
|
||||||
|
case loadingIndicator
|
||||||
|
case header
|
||||||
|
case statuses
|
||||||
|
case footer
|
||||||
|
}
|
||||||
|
enum Item: DiffableTimelineLikeItem {
|
||||||
|
case loadingIndicator
|
||||||
|
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 .loadingIndicator:
|
||||||
|
hasher.combine(0)
|
||||||
|
case let .status(id: id, state: _):
|
||||||
|
hasher.combine(1)
|
||||||
|
hasher.combine(id)
|
||||||
|
case .confirmLoadMore:
|
||||||
|
hasher.combine(2)
|
||||||
|
case let .publicTimelineDescription(local: local):
|
||||||
|
hasher.combine(3)
|
||||||
|
hasher.combine(local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineTableViewController: TuskerNavigationDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineTableViewController: StatusTableViewCellDelegate {
|
||||||
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
} else {
|
||||||
|
cellHeightChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineTableViewController: MenuActionProvider {
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TimelineTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
|
let ids: [String] = indexPaths.compactMap {
|
||||||
|
if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
|
||||||
|
return id
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefetchStatuses(with: ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
|
let ids: [String] = indexPaths.compactMap {
|
||||||
|
if case let .status(id: id, state: _) = dataSource.itemIdentifier(for: $0) {
|
||||||
|
return id
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelPrefetchingStatuses(with: ids)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
// TODO: gonna need a thing to replicate all of EnhancedTableViewController
|
||||||
|
|
||||||
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController, RefreshableViewController {
|
class TimelineViewController: UIViewController, TimelineLikeCollectionViewController, RefreshableViewController {
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
@ -34,7 +36,6 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
|
|
||||||
self.controller = TimelineLikeController(delegate: self)
|
self.controller = TimelineLikeController(delegate: self)
|
||||||
|
|
||||||
self.navigationItem.title = timeline.title
|
|
||||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Timeline"))
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Timeline"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,8 +61,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
config.bottomSeparatorVisibility = .hidden
|
config.bottomSeparatorVisibility = .hidden
|
||||||
}
|
}
|
||||||
if case .status(_, _) = item {
|
if case .status(_, _) = item {
|
||||||
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
config.topSeparatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
|
||||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
config.bottomSeparatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
@ -85,15 +86,15 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
// separate method because InstanceTimelineViewController needs to be able to customize it
|
|
||||||
func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: StatusState) {
|
|
||||||
cell.delegate = self
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, StatusState)> { [unowned self] cell, indexPath, item in
|
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
||||||
self.configureStatusCell(cell, id: item.0, state: item.1)
|
guard case .status(id: let id, state: let state) = item,
|
||||||
|
let status = mastodonController.persistentContainer.status(for: id) else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
cell.mastodonController = mastodonController
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(statusID: id, state: state)
|
||||||
}
|
}
|
||||||
let timelineDescriptionCell = UICollectionView.CellRegistration<PublicTimelineDescriptionCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
let timelineDescriptionCell = UICollectionView.CellRegistration<PublicTimelineDescriptionCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
||||||
guard case .public(let local) = timeline else {
|
guard case .public(let local) = timeline else {
|
||||||
|
@ -107,8 +108,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||||
switch itemIdentifier {
|
switch itemIdentifier {
|
||||||
case .status(id: let id, state: let state):
|
case .status(_, _):
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
|
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: itemIdentifier)
|
||||||
case .loadingIndicator:
|
case .loadingIndicator:
|
||||||
return loadingIndicatorCell(for: indexPath)
|
return loadingIndicatorCell(for: indexPath)
|
||||||
case .confirmLoadMore:
|
case .confirmLoadMore:
|
||||||
|
@ -120,16 +121,15 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// non-private, because ListTimelineViewController needs to be able to reload it from scratch
|
private func applyInitialSnapshot() {
|
||||||
func applyInitialSnapshot() {
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
||||||
if case .public(let local) = timeline,
|
if case .public(let local) = timeline,
|
||||||
(local && !Preferences.shared.hasShownLocalTimelineDescription) ||
|
(local && !Preferences.shared.hasShownLocalTimelineDescription) ||
|
||||||
(!local && !Preferences.shared.hasShownFederatedTimelineDescription) {
|
(!local && !Preferences.shared.hasShownFederatedTimelineDescription) {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.appendSections([.header])
|
snapshot.appendSections([.header])
|
||||||
snapshot.appendItems([.publicTimelineDescription], toSection: .header)
|
snapshot.appendItems([.publicTimelineDescription], toSection: .header)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
}
|
}
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -140,9 +140,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if case .notLoadedInitial = await controller.state {
|
await controller.loadInitial()
|
||||||
await controller.loadInitial()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,7 +160,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
override func viewDidDisappear(_ animated: Bool) {
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
// pruneOffscreenRows()
|
pruneOffscreenRows()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeTimelineDescriptionCell() {
|
private func removeTimelineDescriptionCell() {
|
||||||
|
@ -172,28 +170,27 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
isShowingTimelineDescription = false
|
isShowingTimelineDescription = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// private func pruneOffscreenRows() {
|
private func pruneOffscreenRows() {
|
||||||
// guard let lastVisibleIndexPath = collectionView.indexPathsForVisibleItems.last else {
|
guard let lastVisibleIndexPath = collectionView.indexPathsForVisibleItems.last else {
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
// var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
// guard snapshot.indexOfSection(.statuses) != nil else {
|
guard snapshot.indexOfSection(.statuses) != nil else {
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
// let items = snapshot.itemIdentifiers(inSection: .statuses)
|
let items = snapshot.itemIdentifiers(inSection: .statuses)
|
||||||
// let pageSize = 20
|
let pageSize = 20
|
||||||
// let numberOfPagesToPrune = (items.count - lastVisibleIndexPath.row - 1) / pageSize
|
let numberOfPagesToPrune = (items.count - lastVisibleIndexPath.row - 1) / pageSize
|
||||||
// if numberOfPagesToPrune > 0 {
|
if numberOfPagesToPrune > 0 {
|
||||||
// let itemsToRemove = Array(items.suffix(numberOfPagesToPrune * pageSize))
|
let itemsToRemove = Array(items.suffix(numberOfPagesToPrune * pageSize))
|
||||||
// snapshot.deleteItems(itemsToRemove)
|
snapshot.deleteItems(itemsToRemove)
|
||||||
//
|
}
|
||||||
// dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
//
|
|
||||||
// if case .status(id: let id, state: _) = snapshot.itemIdentifiers(inSection: .statuses).last {
|
if case .status(id: let id, state: _) = snapshot.itemIdentifiers(inSection: .statuses).last {
|
||||||
// older = .before(id: id, count: nil)
|
older = .before(id: id, count: nil)
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
@objc func refresh() {
|
@objc func refresh() {
|
||||||
Task {
|
Task {
|
||||||
|
@ -257,7 +254,7 @@ extension TimelineViewController {
|
||||||
|
|
||||||
var hideSeparators: Bool {
|
var hideSeparators: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .loadingIndicator, .publicTimelineDescription, .confirmLoadMore:
|
case .loadingIndicator, .publicTimelineDescription:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
@ -328,12 +325,10 @@ extension TimelineViewController {
|
||||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||||
let (statuses, _) = try await mastodonController.run(request)
|
let (statuses, _) = try await mastodonController.run(request)
|
||||||
|
|
||||||
guard !statuses.isEmpty else {
|
if !statuses.isEmpty {
|
||||||
return []
|
self.older = .before(id: statuses.last!.id, count: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.older = .before(id: statuses.last!.id, count: nil)
|
|
||||||
|
|
||||||
return await withCheckedContinuation { continuation in
|
return await withCheckedContinuation { continuation in
|
||||||
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
continuation.resume(returning: statuses.map(\.id))
|
continuation.resume(returning: statuses.map(\.id))
|
||||||
|
@ -351,7 +346,8 @@ extension TimelineViewController {
|
||||||
|
|
||||||
extension TimelineViewController: UICollectionViewDelegate {
|
extension TimelineViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||||
guard case .statuses = dataSource.sectionIdentifier(for: indexPath.section) else {
|
guard case .statuses = dataSource.sectionIdentifier(for: indexPath.section),
|
||||||
|
case .status(_, _) = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,7 +401,7 @@ extension TimelineViewController: UICollectionViewDragDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController: TuskerNavigationDelegate {
|
extension TimelineViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController! { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController: MenuActionProvider {
|
extension TimelineViewController: MenuActionProvider {
|
||||||
|
@ -420,16 +416,3 @@ extension TimelineViewController: StatusCollectionViewCellDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineViewController: TabBarScrollableViewController {
|
|
||||||
func tabBarScrollToTop() {
|
|
||||||
collectionView.scrollToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineViewController: StatusBarTappableViewController {
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
collectionView.scrollToTop()
|
|
||||||
return .stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -248,12 +248,3 @@ extension EnhancedNavigationViewController: BackgroundableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EnhancedNavigationViewController: StatusBarTappableViewController {
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
if let topVC = topViewController as? StatusBarTappableViewController {
|
|
||||||
return topVC.handleStatusBarTapped(xPosition: xPosition)
|
|
||||||
}
|
|
||||||
return .continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,6 +11,11 @@ import SafariServices
|
||||||
|
|
||||||
class EnhancedTableViewController: UITableViewController {
|
class EnhancedTableViewController: UITableViewController {
|
||||||
|
|
||||||
|
private var prevScrollToTopOffset: CGPoint? = nil
|
||||||
|
private(set) var isCurrentlyScrollingToTop = false
|
||||||
|
private var prevScrollViewContentOffset: CGPoint?
|
||||||
|
private(set) var scrollViewDirection: CGFloat = 0
|
||||||
|
|
||||||
var dragEnabled = false
|
var dragEnabled = false
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
@ -21,6 +26,38 @@ class EnhancedTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Scroll View Delegate
|
||||||
|
|
||||||
|
override func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||||
|
if let offset = prevScrollToTopOffset {
|
||||||
|
tableView.setContentOffset(offset, animated: true)
|
||||||
|
prevScrollToTopOffset = nil
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
prevScrollToTopOffset = tableView.contentOffset
|
||||||
|
isCurrentlyScrollingToTop = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||||
|
isCurrentlyScrollingToTop = false
|
||||||
|
// add one so it's not technically scrolled all the way to the top,
|
||||||
|
// otherwise there's no way of detecting a status bar press to scroll back down
|
||||||
|
tableView.contentOffset.y -= 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
prevScrollToTopOffset = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
if let prev = prevScrollViewContentOffset {
|
||||||
|
scrollViewDirection = scrollView.contentOffset.y - prev.y
|
||||||
|
}
|
||||||
|
prevScrollViewContentOffset = scrollView.contentOffset
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Table View Delegate
|
// MARK: Table View Delegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
@ -80,13 +117,10 @@ extension EnhancedTableViewController: UITableViewDragDelegate {
|
||||||
|
|
||||||
extension EnhancedTableViewController: TabBarScrollableViewController {
|
extension EnhancedTableViewController: TabBarScrollableViewController {
|
||||||
func tabBarScrollToTop() {
|
func tabBarScrollToTop() {
|
||||||
tableView.scrollToTop()
|
if scrollViewShouldScrollToTop(tableView) {
|
||||||
}
|
let topOffset = CGPoint(x: 0, y: -tableView.adjustedContentInset.top)
|
||||||
}
|
tableView.setContentOffset(topOffset, animated: true)
|
||||||
|
scrollViewDidScrollToTop(tableView)
|
||||||
extension EnhancedTableViewController: StatusBarTappableViewController {
|
}
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
tableView.scrollToTop()
|
|
||||||
return .stop
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,23 +60,12 @@ extension MenuActionProvider {
|
||||||
draft.visibility = .direct
|
draft.visibility = .direct
|
||||||
self.navigationDelegate?.compose(editing: draft)
|
self.navigationDelegate?.compose(editing: draft)
|
||||||
}),
|
}),
|
||||||
UIDeferredMenuElement.uncached({ @MainActor [unowned self] elementHandler in
|
UIDeferredMenuElement.uncached({ (elementHandler) in
|
||||||
let relationship = Task {
|
Task { @MainActor in
|
||||||
await fetchRelationship(accountID: accountID, mastodonController: mastodonController)
|
if let action = await self.followAction(for: accountID, mastodonController: mastodonController) {
|
||||||
}
|
elementHandler([action])
|
||||||
// workaround for #198, may result in showing outdated relationship, so only do so where necessary
|
} else {
|
||||||
if ProcessInfo.processInfo.isiOSAppOnMac,
|
elementHandler([])
|
||||||
let mo = mastodonController.persistentContainer.relationship(forAccount: accountID),
|
|
||||||
let action = self.followAction(for: mo, mastodonController: mastodonController) {
|
|
||||||
elementHandler([action])
|
|
||||||
} else {
|
|
||||||
Task { @MainActor in
|
|
||||||
if let relationship = await relationship.value,
|
|
||||||
let action = self.followAction(for: relationship, mastodonController: mastodonController) {
|
|
||||||
elementHandler([action])
|
|
||||||
} else {
|
|
||||||
elementHandler([])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -392,13 +381,16 @@ extension MenuActionProvider {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
private func followAction(for accountID: String, mastodonController: MastodonController) async -> UIMenuElement? {
|
||||||
private func followAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement? {
|
guard let ownAccount = try? await mastodonController.getOwnAccount(),
|
||||||
guard let ownAccount = mastodonController.account,
|
accountID != ownAccount.id else {
|
||||||
relationship.accountID != ownAccount.id else {
|
return nil
|
||||||
|
}
|
||||||
|
let request = Client.getRelationships(accounts: [accountID])
|
||||||
|
guard let (relationships, _) = try? await mastodonController.run(request),
|
||||||
|
let relationship = relationships.first else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let accountID = relationship.accountID
|
|
||||||
let following = relationship.following
|
let following = relationship.following
|
||||||
return createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus") { _ in
|
return createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus") { _ in
|
||||||
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
||||||
|
@ -420,19 +412,6 @@ extension MenuActionProvider {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> RelationshipMO? {
|
|
||||||
let req = Client.getRelationships(accounts: [accountID])
|
|
||||||
guard let (relationships, _) = try? await mastodonController.run(req),
|
|
||||||
let r = relationships.first else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return await withCheckedContinuation { continuation in
|
|
||||||
mastodonController.persistentContainer.addOrUpdate(relationship: r, in: mastodonController.persistentContainer.viewContext) { mo in
|
|
||||||
continuation.resume(returning: mo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MenuPreviewHelper {
|
struct MenuPreviewHelper {
|
||||||
static func willPerformPreviewAction(animator: UIContextMenuInteractionCommitAnimating, presenter: UIViewController) {
|
static func willPerformPreviewAction(animator: UIContextMenuInteractionCommitAnimating, presenter: UIViewController) {
|
||||||
if let viewController = animator.previewViewController {
|
if let viewController = animator.previewViewController {
|
||||||
|
|
|
@ -105,12 +105,3 @@ extension SegmentedPageViewController: BackgroundableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SegmentedPageViewController: StatusBarTappableViewController {
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
if let current = pageControllers[currentIndex] as? StatusBarTappableViewController {
|
|
||||||
return current.handleStatusBarTapped(xPosition: xPosition)
|
|
||||||
}
|
|
||||||
return .continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -135,6 +135,12 @@ class SplitNavigationController: UIViewController {
|
||||||
updateSecondaryNavVisibility()
|
updateSecondaryNavVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
override func viewWillLayoutSubviews() {
|
override func viewWillLayoutSubviews() {
|
||||||
super.viewWillLayoutSubviews()
|
super.viewWillLayoutSubviews()
|
||||||
|
|
||||||
|
@ -239,24 +245,7 @@ class SplitNavigationController: UIViewController {
|
||||||
self.updateSecondaryNavVisibility()
|
self.updateSecondaryNavVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension SplitNavigationController: StatusBarTappableViewController {
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
|
||||||
let vcs = viewControllers
|
|
||||||
if !canShowSecondaryNav || vcs.count < 2 {
|
|
||||||
return (vcs.first! as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue
|
|
||||||
} else {
|
|
||||||
let positionInRoot = rootNav.view.convert(CGPoint(x: xPosition, y: 0), from: view)
|
|
||||||
let positionInSecondary = secondaryNav.view.convert(CGPoint(x: xPosition, y: 0), from: view)
|
|
||||||
if rootNav.view.bounds.contains(positionInRoot) {
|
|
||||||
return (rootNav.topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: positionInRoot.x) ?? .continue
|
|
||||||
} else if secondaryNav.view.bounds.contains(positionInSecondary) {
|
|
||||||
return (secondaryNav.topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: positionInRoot.x) ?? .continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return .continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SplitRootNavigationController: UINavigationController {
|
private class SplitRootNavigationController: UINavigationController {
|
||||||
|
@ -282,8 +271,7 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll
|
||||||
override var next: UIResponder? {
|
override var next: UIResponder? {
|
||||||
// ordinarily, the next responder in the chain would be the SplitNavigationController's view
|
// ordinarily, the next responder in the chain would be the SplitNavigationController's view
|
||||||
// but that would bypass the VC in the root nav, so we reroute the repsonder chain to include it
|
// but that would bypass the VC in the root nav, so we reroute the repsonder chain to include it
|
||||||
// first seems to be nil when using the view debugger for some reason, so in that case, defer to super
|
owner.viewControllers.first!.view
|
||||||
owner.viewControllers.first?.view ?? super.next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureSecondarySplitCloseButton(for viewController: UIViewController) {
|
private func configureSecondarySplitCloseButton(for viewController: UIViewController) {
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
//
|
|
||||||
// StatusBarTappableViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/1/22.
|
|
||||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
protocol StatusBarTappableViewController: UIViewController {
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult
|
|
||||||
}
|
|
|
@ -30,6 +30,22 @@ extension StatusTablePrefetching {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cancelPrefetchingStatuses(with ids: [String]) {
|
||||||
|
let context = apiController.persistentContainer.prefetchBackgroundContext
|
||||||
|
context.perform {
|
||||||
|
guard let statuses = getStatusesWith(ids: ids, in: context) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for status in statuses {
|
||||||
|
guard let avatar = status.account.avatar else { continue }
|
||||||
|
ImageCache.avatars.cancelWithoutCallback(avatar)
|
||||||
|
for attachment in status.attachments where attachment.kind == .image {
|
||||||
|
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func getStatusesWith(ids: [String], in context: NSManagedObjectContext) -> [StatusMO]? {
|
fileprivate func getStatusesWith(ids: [String], in context: NSManagedObjectContext) -> [StatusMO]? {
|
||||||
|
|
|
@ -205,7 +205,7 @@ class UserActivityManager {
|
||||||
rootController.segmentedControl.selectedSegmentIndex = index
|
rootController.segmentedControl.selectedSegmentIndex = index
|
||||||
rootController.selectPage(at: index, animated: false)
|
rootController.selectPage(at: index, animated: false)
|
||||||
default:
|
default:
|
||||||
let timeline = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
let timeline = TimelineTableViewController(for: timeline, mastodonController: mastodonController)
|
||||||
navigationController.pushViewController(timeline, animated: false)
|
navigationController.pushViewController(timeline, animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ actor TimelineLikeController<Item> {
|
||||||
willSet {
|
willSet {
|
||||||
guard state.canTransition(to: newValue) else {
|
guard state.canTransition(to: newValue) else {
|
||||||
logger.error("State \(self.state.debugDescription, privacy: .public) cannot transition to \(newValue.debugDescription, privacy: .public)")
|
logger.error("State \(self.state.debugDescription, privacy: .public) cannot transition to \(newValue.debugDescription, privacy: .public)")
|
||||||
fatalError("State \(state) cannot transition to \(newValue)")
|
preconditionFailure("cannot transition to state")
|
||||||
}
|
}
|
||||||
logger.debug("State: \(self.state.debugDescription, privacy: .public) -> \(newValue.debugDescription, privacy: .public)")
|
logger.debug("State: \(self.state.debugDescription, privacy: .public) -> \(newValue.debugDescription, privacy: .public)")
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ actor TimelineLikeController<Item> {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadInitial() async {
|
func loadInitial() async {
|
||||||
guard state == .notLoadedInitial || state == .idle else {
|
guard state == .notLoadedInitial else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let token = LoadAttemptToken()
|
let token = LoadAttemptToken()
|
||||||
|
@ -65,8 +65,6 @@ actor TimelineLikeController<Item> {
|
||||||
await loadingIndicator.end()
|
await loadingIndicator.end()
|
||||||
await emit(event: .replaceAllItems(items, token))
|
await emit(event: .replaceAllItems(items, token))
|
||||||
state = .idle
|
state = .idle
|
||||||
} catch is CancellationError {
|
|
||||||
return
|
|
||||||
} catch {
|
} catch {
|
||||||
await loadingIndicator.end()
|
await loadingIndicator.end()
|
||||||
await emit(event: .loadAllError(error, token))
|
await emit(event: .loadAllError(error, token))
|
||||||
|
@ -87,8 +85,6 @@ actor TimelineLikeController<Item> {
|
||||||
}
|
}
|
||||||
await emit(event: .prependItems(items, token))
|
await emit(event: .prependItems(items, token))
|
||||||
state = .idle
|
state = .idle
|
||||||
} catch is CancellationError {
|
|
||||||
return
|
|
||||||
} catch {
|
} catch {
|
||||||
await emit(event: .loadNewerError(error, token))
|
await emit(event: .loadNewerError(error, token))
|
||||||
state = .idle
|
state = .idle
|
||||||
|
@ -100,11 +96,7 @@ actor TimelineLikeController<Item> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let token = LoadAttemptToken()
|
let token = LoadAttemptToken()
|
||||||
guard await delegate.canLoadOlder(),
|
guard await delegate.canLoadOlder() else {
|
||||||
// Make sure we're still in the idle state before continuing on, since that may have chnaged while waiting for user input.
|
|
||||||
// If the load more cell appears, then the users scrolls up and back down, the VC may kick off a second loadOlder task
|
|
||||||
// but we only want one to proceed. The actor prevents a data race, and this prevents multiple simultaneousl loadOlder tasks from running.
|
|
||||||
state == .idle else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state = .loadingOlder(token, hasAddedLoadingIndicator: false)
|
state = .loadingOlder(token, hasAddedLoadingIndicator: false)
|
||||||
|
@ -117,8 +109,6 @@ actor TimelineLikeController<Item> {
|
||||||
await loadingIndicator.end()
|
await loadingIndicator.end()
|
||||||
await emit(event: .appendItems(items, token))
|
await emit(event: .appendItems(items, token))
|
||||||
state = .idle
|
state = .idle
|
||||||
} catch is CancellationError {
|
|
||||||
return
|
|
||||||
} catch {
|
} catch {
|
||||||
await loadingIndicator.end()
|
await loadingIndicator.end()
|
||||||
await emit(event: .loadOlderError(error, token))
|
await emit(event: .loadOlderError(error, token))
|
||||||
|
@ -133,7 +123,7 @@ actor TimelineLikeController<Item> {
|
||||||
private func emit(event: Event) async {
|
private func emit(event: Event) async {
|
||||||
guard state.canEmit(event: event) else {
|
guard state.canEmit(event: event) else {
|
||||||
logger.error("State \(self.state.debugDescription, privacy: .public) cannot emit event: \(event.debugDescription, privacy: .public)")
|
logger.error("State \(self.state.debugDescription, privacy: .public) cannot emit event: \(event.debugDescription, privacy: .public)")
|
||||||
fatalError("State \(state) cannot emit event: \(event)")
|
preconditionFailure("state cannot emit event")
|
||||||
}
|
}
|
||||||
switch event {
|
switch event {
|
||||||
case .addLoadingIndicator:
|
case .addLoadingIndicator:
|
||||||
|
@ -181,14 +171,14 @@ actor TimelineLikeController<Item> {
|
||||||
switch self {
|
switch self {
|
||||||
case .notLoadedInitial:
|
case .notLoadedInitial:
|
||||||
switch to {
|
switch to {
|
||||||
case .loadingInitial(_, _):
|
case .loadingInitial(_, hasAddedLoadingIndicator: _):
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case .idle:
|
case .idle:
|
||||||
switch to {
|
switch to {
|
||||||
case .loadingInitial(_, _), .loadingNewer(_), .loadingOlder(_, _):
|
case .loadingNewer(_), .loadingOlder(_, _):
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -11,7 +11,7 @@ import SafariServices
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
||||||
var apiController: MastodonController! { get }
|
var apiController: MastodonController { get }
|
||||||
|
|
||||||
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController
|
func conversation(mainStatusID: String, state: StatusState) -> ConversationTableViewController
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ extension TuskerNavigationDelegate {
|
||||||
|
|
||||||
func selected(account accountID: String) {
|
func selected(account accountID: String) {
|
||||||
// don't open if the account is the same as the current one
|
// don't open if the account is the same as the current one
|
||||||
if let profileController = self as? ProfileStatusesViewController,
|
if let profileController = self as? ProfileViewController,
|
||||||
profileController.accountID == accountID {
|
profileController.accountID == accountID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,7 @@ extension TuskerNavigationDelegate {
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,17 @@
|
||||||
/// From https://github.com/woltapp/blurhash/blob/b23214ddcab803fe1ec9a3e6b20558caf33a23a5/Swift/BlurHashDecode.swift
|
/// From https://github.com/woltapp/blurhash/blob/b23214ddcab803fe1ec9a3e6b20558caf33a23a5/Swift/BlurHashDecode.swift
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
// blurhashes are disabled in debug builds because this code is hideously slow when not optimized by the compiler
|
||||||
|
#if DEBUG
|
||||||
|
fileprivate let blurHashesEnabled = ProcessInfo.processInfo.environment.keys.contains("DEBUG_BLUR_HASH")
|
||||||
|
#else
|
||||||
|
fileprivate let blurHashesEnabled = true
|
||||||
|
#endif
|
||||||
|
|
||||||
extension UIImage {
|
extension UIImage {
|
||||||
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
|
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
|
||||||
guard blurHash.count >= 6 else { return nil }
|
guard blurHashesEnabled,
|
||||||
|
blurHash.count >= 6 else { return nil }
|
||||||
|
|
||||||
let sizeFlag = String(blurHash[0]).decode83()
|
let sizeFlag = String(blurHash[0]).decode83()
|
||||||
let numY = (sizeFlag / 9) + 1
|
let numY = (sizeFlag / 9) + 1
|
||||||
|
|
|
@ -26,6 +26,7 @@ class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
var attachment: Attachment!
|
var attachment: Attachment!
|
||||||
var index: Int!
|
var index: Int!
|
||||||
|
var expectedSize: CGSize!
|
||||||
|
|
||||||
private var attachmentRequest: ImageCache.Request?
|
private var attachmentRequest: ImageCache.Request?
|
||||||
private var source: Source?
|
private var source: Source?
|
||||||
|
@ -36,12 +37,13 @@ class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
private var isGrayscale = false
|
private var isGrayscale = false
|
||||||
|
|
||||||
init(attachment: Attachment, index: Int) {
|
init(attachment: Attachment, index: Int, expectedSize: CGSize) {
|
||||||
super.init(image: nil)
|
super.init(image: nil)
|
||||||
commonInit()
|
commonInit()
|
||||||
|
|
||||||
self.attachment = attachment
|
self.attachment = attachment
|
||||||
self.index = index
|
self.index = index
|
||||||
|
self.expectedSize = expectedSize
|
||||||
loadAttachment()
|
loadAttachment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,14 +106,24 @@ class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
func loadAttachment() {
|
func loadAttachment() {
|
||||||
guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else {
|
guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else {
|
||||||
fatalError("invalid attachment type")
|
preconditionFailure("invalid attachment type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let hash = attachment.blurHash {
|
if let hash = attachment.blurHash {
|
||||||
AttachmentView.queue.async { [weak self] in
|
AttachmentView.queue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
let size: CGSize
|
||||||
|
if let meta = self.attachment.meta,
|
||||||
|
let width = meta.width, let height = meta.height {
|
||||||
|
size = CGSize(width: width, height: height)
|
||||||
|
} else if let orig = self.attachment.meta?.original,
|
||||||
|
let width = orig.width, let height = orig.height {
|
||||||
|
size = CGSize(width: width, height: height)
|
||||||
|
} else {
|
||||||
|
size = self.expectedSize
|
||||||
|
}
|
||||||
|
|
||||||
guard var preview = UIImage(blurHash: hash, size: self.blurHashSize()) else {
|
guard var preview = UIImage(blurHash: hash, size: size) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,28 +149,7 @@ class AttachmentView: GIFImageView {
|
||||||
case .gifv:
|
case .gifv:
|
||||||
loadGifv()
|
loadGifv()
|
||||||
default:
|
default:
|
||||||
fatalError("invalid attachment type")
|
preconditionFailure("invalid attachment type")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func blurHashSize() -> CGSize {
|
|
||||||
if let meta = self.attachment.meta {
|
|
||||||
let aspectRatio: CGFloat
|
|
||||||
if let width = meta.width, let height = meta.height {
|
|
||||||
aspectRatio = CGFloat(width) / CGFloat(height)
|
|
||||||
} else if let orig = meta.original,
|
|
||||||
let width = orig.width, let height = orig.height {
|
|
||||||
aspectRatio = CGFloat(width) / CGFloat(height)
|
|
||||||
} else {
|
|
||||||
return CGSize(width: 32, height: 32)
|
|
||||||
}
|
|
||||||
if aspectRatio > 1 {
|
|
||||||
return CGSize(width: 32, height: 32 / aspectRatio)
|
|
||||||
} else {
|
|
||||||
return CGSize(width: 32 * aspectRatio, height: 32)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return CGSize(width: 32, height: 32)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -257,7 +257,7 @@ class AttachmentsContainerView: UIView {
|
||||||
}
|
}
|
||||||
let size = CGSize(width: width, height: height)
|
let size = CGSize(width: width, height: height)
|
||||||
|
|
||||||
let attachmentView = AttachmentView(attachment: attachments[index], index: index)
|
let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size)
|
||||||
attachmentView.delegate = delegate
|
attachmentView.delegate = delegate
|
||||||
attachmentView.translatesAutoresizingMaskIntoConstraints = false
|
attachmentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
attachmentView.accessibilityLabel = String(format: NSLocalizedString("Attachment %d", comment: "attachment at index accessiblity label"), index + 1)
|
attachmentView.accessibilityLabel = String(format: NSLocalizedString("Attachment %d", comment: "attachment at index accessiblity label"), index + 1)
|
||||||
|
|
|
@ -48,7 +48,7 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
|
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
|
||||||
adultLabel.isHidden = true
|
adultLabel.isHidden = true
|
||||||
descriptionTextView.setTextFromHtml(instance.shortDescription ?? instance.description)
|
descriptionTextView.setTextFromHtml(instance.description)
|
||||||
|
|
||||||
if let thumbnail = instance.thumbnail {
|
if let thumbnail = instance.thumbnail {
|
||||||
updateThumbnail(url: thumbnail)
|
updateThumbnail(url: thumbnail)
|
||||||
|
|
|
@ -119,12 +119,12 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
let request = Client.getRelationships(accounts: [accountID])
|
let request = Client.getRelationships(accounts: [accountID])
|
||||||
mastodonController.run(request) { [weak self] (response) in
|
mastodonController.run(request) { [weak self] (response) in
|
||||||
guard let mastodonController = self?.mastodonController,
|
guard let self = self,
|
||||||
case let .success(results, _) = response,
|
case let .success(results, _) = response,
|
||||||
let relationship = results.first else {
|
let relationship = results.first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,19 +168,16 @@ class StatusCardView: UIView {
|
||||||
private func loadBlurHash() {
|
private func loadBlurHash() {
|
||||||
guard let card = card, let hash = card.blurhash else { return }
|
guard let card = card, let hash = card.blurhash else { return }
|
||||||
|
|
||||||
|
let imageViewSize = self.imageView.bounds.size
|
||||||
|
|
||||||
AttachmentView.queue.async { [weak self] in
|
AttachmentView.queue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
let size: CGSize
|
let size: CGSize
|
||||||
if let width = card.width, let height = card.height {
|
if let width = card.width, let height = card.height {
|
||||||
let aspectRatio = CGFloat(width) / CGFloat(height)
|
size = CGSize(width: width, height: height)
|
||||||
if aspectRatio > 1 {
|
|
||||||
size = CGSize(width: 32, height: 32 / aspectRatio)
|
|
||||||
} else {
|
|
||||||
size = CGSize(width: 32 * aspectRatio, height: 32)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
size = CGSize(width: 32, height: 32)
|
size = imageViewSize
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let preview = UIImage(blurHash: hash, size: size) else {
|
guard let preview = UIImage(blurHash: hash, size: size) else {
|
||||||
|
|
|
@ -29,8 +29,9 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
||||||
var reblogButton: UIButton { get }
|
var reblogButton: UIButton { get }
|
||||||
var moreButton: UIButton { get }
|
var moreButton: UIButton { get }
|
||||||
|
|
||||||
var delegate: StatusCollectionViewCellDelegate? { get }
|
// TODO: why is one of these ! and the other ?
|
||||||
var mastodonController: MastodonController! { get }
|
var mastodonController: MastodonController! { get }
|
||||||
|
var delegate: StatusCollectionViewCellDelegate? { get }
|
||||||
|
|
||||||
var showStatusAutomatically: Bool { get }
|
var showStatusAutomatically: Bool { get }
|
||||||
var showReplyIndicator: Bool { get }
|
var showReplyIndicator: Bool { get }
|
||||||
|
@ -49,8 +50,6 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
||||||
extension StatusCollectionViewCell {
|
extension StatusCollectionViewCell {
|
||||||
static var avatarImageViewSize: CGFloat { 50 }
|
static var avatarImageViewSize: CGFloat { 50 }
|
||||||
|
|
||||||
var mastodonController: MastodonController! { delegate?.apiController }
|
|
||||||
|
|
||||||
func baseCreateObservers() {
|
func baseCreateObservers() {
|
||||||
mastodonController.persistentContainer.statusSubject
|
mastodonController.persistentContainer.statusSubject
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -76,6 +75,8 @@ extension StatusCollectionViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
func doUpdateUI(status: StatusMO) {
|
func doUpdateUI(status: StatusMO) {
|
||||||
|
precondition(delegate != nil, "StatusCollectionViewCell must have delegate")
|
||||||
|
|
||||||
statusID = status.id
|
statusID = status.id
|
||||||
accountID = status.account.id
|
accountID = status.account.id
|
||||||
|
|
||||||
|
|
|
@ -12,15 +12,12 @@ import Combine
|
||||||
|
|
||||||
class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
|
class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
|
||||||
|
|
||||||
static let separatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
|
|
||||||
|
|
||||||
// MARK: Subviews
|
// MARK: Subviews
|
||||||
|
|
||||||
private lazy var reblogLabel = EmojiLabel().configure {
|
private lazy var reblogLabel = EmojiLabel().configure {
|
||||||
$0.textColor = .secondaryLabel
|
$0.textColor = .secondaryLabel
|
||||||
// this needs to have a higher priorty than the content container's zero height constraint
|
// this needs to have a higher priorty than the content container's zero height constraint
|
||||||
$0.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
$0.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||||
$0.isUserInteractionEnabled = true
|
|
||||||
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
|
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +51,6 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
$0.isUserInteractionEnabled = true
|
$0.isUserInteractionEnabled = true
|
||||||
$0.addInteraction(UIContextMenuInteraction(delegate: self))
|
$0.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
$0.addInteraction(UIDragInteraction(delegate: self))
|
$0.addInteraction(UIDragInteraction(delegate: self))
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
|
||||||
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +113,6 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var contentWarningLabel = EmojiLabel().configure {
|
private(set) lazy var contentWarningLabel = EmojiLabel().configure {
|
||||||
$0.numberOfLines = 0
|
|
||||||
$0.textColor = .secondaryLabel
|
$0.textColor = .secondaryLabel
|
||||||
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
||||||
.traits: [
|
.traits: [
|
||||||
|
@ -195,29 +190,21 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
private(set) lazy var replyButton = UIButton().configure {
|
private(set) lazy var replyButton = UIButton().configure {
|
||||||
$0.setImage(UIImage(systemName: "arrowshape.turn.up.left.fill"), for: .normal)
|
$0.setImage(UIImage(systemName: "arrowshape.turn.up.left.fill"), for: .normal)
|
||||||
$0.addTarget(self, action: #selector(replyPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(replyPressed), for: .touchUpInside)
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var favoriteButton = UIButton().configure {
|
private(set) lazy var favoriteButton = UIButton().configure {
|
||||||
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
||||||
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var reblogButton = UIButton().configure {
|
private(set) lazy var reblogButton = UIButton().configure {
|
||||||
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
||||||
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) lazy var moreButton = UIButton().configure {
|
let moreButton = UIButton().configure {
|
||||||
$0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
|
$0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
|
||||||
$0.showsMenuAsPrimaryAction = true
|
$0.showsMenuAsPrimaryAction = true
|
||||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var actionButtons: [UIButton] {
|
|
||||||
[replyButton, favoriteButton, reblogButton, moreButton]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Cell state
|
// MARK: Cell state
|
||||||
|
@ -227,8 +214,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
private var mainContainerBottomToActionsConstraint: NSLayoutConstraint!
|
private var mainContainerBottomToActionsConstraint: NSLayoutConstraint!
|
||||||
private var mainContainerBottomToSelfConstraint: NSLayoutConstraint!
|
private var mainContainerBottomToSelfConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
weak var overrideMastodonController: MastodonController?
|
weak var mastodonController: MastodonController!
|
||||||
var mastodonController: MastodonController! { overrideMastodonController ?? delegate?.apiController }
|
|
||||||
weak var delegate: StatusCollectionViewCellDelegate?
|
weak var delegate: StatusCollectionViewCellDelegate?
|
||||||
var showStatusAutomatically: Bool {
|
var showStatusAutomatically: Bool {
|
||||||
// TODO: needed once conversation controller refactored
|
// TODO: needed once conversation controller refactored
|
||||||
|
@ -238,7 +224,10 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
// TODO: needed once conversation controller refactored
|
// TODO: needed once conversation controller refactored
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
var showPinned: Bool = false
|
var showPinned: Bool {
|
||||||
|
// TODO: needed once profile controller refactored
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// alas these need to be internal so they're accessible from the protocol extensions
|
// alas these need to be internal so they're accessible from the protocol extensions
|
||||||
var statusID: String!
|
var statusID: String!
|
||||||
|
@ -263,7 +252,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
|
|
||||||
mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: reblogLabel.bottomAnchor, constant: 4)
|
mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: reblogLabel.bottomAnchor, constant: 4)
|
||||||
mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8)
|
mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8)
|
||||||
mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4)
|
mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor)
|
||||||
mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6)
|
mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6)
|
||||||
|
|
||||||
let metaIndicatorsBottomConstraint = metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -6)
|
let metaIndicatorsBottomConstraint = metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -6)
|
||||||
|
@ -623,31 +612,3 @@ extension TimelineStatusCollectionViewCell: UIDragInteractionDelegate {
|
||||||
return dragItemsForAccount()
|
return dragItemsForAccount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineStatusCollectionViewCell: UIPointerInteractionDelegate {
|
|
||||||
func pointerInteraction(_ interaction: UIPointerInteraction, regionFor request: UIPointerRegionRequest, defaultRegion: UIPointerRegion) -> UIPointerRegion? {
|
|
||||||
if interaction.view === avatarImageView {
|
|
||||||
return defaultRegion
|
|
||||||
} else if let button = interaction.view as? UIButton,
|
|
||||||
actionButtons.contains(button) {
|
|
||||||
var rect = button.convert(button.imageView!.bounds, from: button.imageView!)
|
|
||||||
rect = rect.insetBy(dx: -24, dy: -24)
|
|
||||||
return UIPointerRegion(rect: rect)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
|
||||||
if interaction.view === avatarImageView {
|
|
||||||
let preview = UITargetedPreview(view: avatarImageView)
|
|
||||||
return UIPointerStyle(effect: .lift(preview))
|
|
||||||
} else if let button = interaction.view as? UIButton,
|
|
||||||
actionButtons.contains(button) {
|
|
||||||
let preview = UITargetedPreview(view: button.imageView!)
|
|
||||||
var rect = button.convert(button.imageView!.bounds, from: button.imageView!)
|
|
||||||
rect = rect.insetBy(dx: -8, dy: -8)
|
|
||||||
return UIPointerStyle(effect: .highlight(preview), shape: .roundedRect(rect))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// XCBSessionType.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/23/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum XCBSessionType {
|
||||||
|
case postStatus
|
||||||
|
}
|
Loading…
Reference in New Issue