Add pinned timeline customization
This commit is contained in:
parent
795146cde4
commit
4dc108f782
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum Timeline: Equatable {
|
public enum Timeline: Equatable, Hashable {
|
||||||
case home
|
case home
|
||||||
case `public`(local: Bool)
|
case `public`(local: Bool)
|
||||||
case tag(hashtag: String)
|
case tag(hashtag: String)
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
||||||
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
||||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; };
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; };
|
||||||
D61F759929384D4D00C0B37F /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759829384D4D00C0B37F /* FiltersView.swift */; };
|
D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759829384D4D00C0B37F /* CustomizeTimelinesView.swift */; };
|
||||||
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759A29384F9C00C0B37F /* FilterMO.swift */; };
|
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759A29384F9C00C0B37F /* FilterMO.swift */; };
|
||||||
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759C2938574B00C0B37F /* FilterRow.swift */; };
|
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759C2938574B00C0B37F /* FilterRow.swift */; };
|
||||||
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */; };
|
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */; };
|
||||||
|
@ -186,6 +186,10 @@
|
||||||
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
||||||
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; };
|
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; };
|
||||||
D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DE828D962C2006341DA /* TimelineLikeController.swift */; };
|
D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DE828D962C2006341DA /* TimelineLikeController.swift */; };
|
||||||
|
D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */; };
|
||||||
|
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A76E229524D2A001DA1B3 /* ListMO.swift */; };
|
||||||
|
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A76E729527884001DA1B3 /* PinnedTimelinesView.swift */; };
|
||||||
|
D68A76EA295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A76E9295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift */; };
|
||||||
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
||||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
||||||
|
@ -424,7 +428,7 @@
|
||||||
D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = "<group>"; };
|
D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = "<group>"; };
|
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = "<group>"; };
|
||||||
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = "<group>"; };
|
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = "<group>"; };
|
||||||
D61F759829384D4D00C0B37F /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; };
|
D61F759829384D4D00C0B37F /* CustomizeTimelinesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeTimelinesView.swift; sourceTree = "<group>"; };
|
||||||
D61F759A29384F9C00C0B37F /* FilterMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMO.swift; sourceTree = "<group>"; };
|
D61F759A29384F9C00C0B37F /* FilterMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMO.swift; sourceTree = "<group>"; };
|
||||||
D61F759C2938574B00C0B37F /* FilterRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterRow.swift; sourceTree = "<group>"; };
|
D61F759C2938574B00C0B37F /* FilterRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterRow.swift; sourceTree = "<group>"; };
|
||||||
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparator.swift; sourceTree = "<group>"; };
|
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparator.swift; sourceTree = "<group>"; };
|
||||||
|
@ -560,6 +564,10 @@
|
||||||
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
||||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; };
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; };
|
||||||
D6895DE828D962C2006341DA /* TimelineLikeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeController.swift; sourceTree = "<group>"; };
|
D6895DE828D962C2006341DA /* TimelineLikeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeController.swift; sourceTree = "<group>"; };
|
||||||
|
D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPreferences.swift; sourceTree = "<group>"; };
|
||||||
|
D68A76E229524D2A001DA1B3 /* ListMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListMO.swift; sourceTree = "<group>"; };
|
||||||
|
D68A76E729527884001DA1B3 /* PinnedTimelinesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTimelinesView.swift; sourceTree = "<group>"; };
|
||||||
|
D68A76E9295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHashtagPinnedTimelineView.swift; sourceTree = "<group>"; };
|
||||||
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -798,14 +806,16 @@
|
||||||
path = "Instance Cell";
|
path = "Instance Cell";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D61F759729384D4200C0B37F /* Filters */ = {
|
D61F759729384D4200C0B37F /* Customize Timelines */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D61F759829384D4D00C0B37F /* FiltersView.swift */,
|
D61F759829384D4D00C0B37F /* CustomizeTimelinesView.swift */,
|
||||||
D61F759C2938574B00C0B37F /* FilterRow.swift */,
|
D61F759C2938574B00C0B37F /* FilterRow.swift */,
|
||||||
D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */,
|
D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */,
|
||||||
|
D68A76E729527884001DA1B3 /* PinnedTimelinesView.swift */,
|
||||||
|
D68A76E9295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift */,
|
||||||
);
|
);
|
||||||
path = Filters;
|
path = "Customize Timelines";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D623A53B2635F4E20095BD04 /* Poll */ = {
|
D623A53B2635F4E20095BD04 /* Poll */ = {
|
||||||
|
@ -895,6 +905,8 @@
|
||||||
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
||||||
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
|
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
|
||||||
D6D706A62948D4D0000827ED /* TimlineState.swift */,
|
D6D706A62948D4D0000827ED /* TimlineState.swift */,
|
||||||
|
D68A76E229524D2A001DA1B3 /* ListMO.swift */,
|
||||||
|
D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */,
|
||||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
||||||
);
|
);
|
||||||
|
@ -924,7 +936,7 @@
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
D627943C23A5635D00D38C68 /* Explore */,
|
D627943C23A5635D00D38C68 /* Explore */,
|
||||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||||
D61F759729384D4200C0B37F /* Filters */,
|
D61F759729384D4200C0B37F /* Customize Timelines */,
|
||||||
D641C788213DD86D004B4513 /* Large Image */,
|
D641C788213DD86D004B4513 /* Large Image */,
|
||||||
D627944B23A9A02400D38C68 /* Lists */,
|
D627944B23A9A02400D38C68 /* Lists */,
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
|
@ -1871,6 +1883,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 */,
|
||||||
|
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
||||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
||||||
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
||||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
||||||
|
@ -1979,6 +1992,7 @@
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
D61F75882932DB6000C0B37F /* StatusSwipeAction.swift in Sources */,
|
D61F75882932DB6000C0B37F /* StatusSwipeAction.swift in Sources */,
|
||||||
|
D68A76EA295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift in Sources */,
|
||||||
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */,
|
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */,
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||||
|
@ -2006,7 +2020,7 @@
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||||
D61F759929384D4D00C0B37F /* FiltersView.swift in Sources */,
|
D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */,
|
||||||
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
|
@ -2040,6 +2054,7 @@
|
||||||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
||||||
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */,
|
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */,
|
||||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||||
|
D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */,
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||||
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
|
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
|
||||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||||
|
@ -2073,6 +2088,7 @@
|
||||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||||
|
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
||||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||||
D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */,
|
D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */,
|
||||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||||
|
|
|
@ -40,6 +40,7 @@ class MastodonController: ObservableObject {
|
||||||
|
|
||||||
let instanceURL: URL
|
let instanceURL: URL
|
||||||
var accountInfo: LocalData.UserAccountInfo?
|
var accountInfo: LocalData.UserAccountInfo?
|
||||||
|
var accountPreferences: AccountPreferences!
|
||||||
|
|
||||||
let client: Client!
|
let client: Client!
|
||||||
|
|
||||||
|
@ -154,6 +155,13 @@ class MastodonController: ObservableObject {
|
||||||
// are available when Filterers are constructed
|
// are available when Filterers are constructed
|
||||||
loadCachedFilters()
|
loadCachedFilters()
|
||||||
|
|
||||||
|
if let existing = try? persistentContainer.viewContext.fetch(AccountPreferences.fetchRequest(account: accountInfo!)).first {
|
||||||
|
accountPreferences = existing
|
||||||
|
} else {
|
||||||
|
accountPreferences = AccountPreferences.default(account: accountInfo!, context: persistentContainer.viewContext)
|
||||||
|
persistentContainer.save(context: persistentContainer.viewContext)
|
||||||
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
async let ownAccount = try getOwnAccount()
|
async let ownAccount = try getOwnAccount()
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// AccountPreferences.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/19/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@objc(AccountPreferences)
|
||||||
|
public final class AccountPreferences: NSManagedObject {
|
||||||
|
|
||||||
|
@nonobjc class func fetchRequest(account: LocalData.UserAccountInfo) -> NSFetchRequest<AccountPreferences> {
|
||||||
|
let req = NSFetchRequest<AccountPreferences>(entityName: "AccountPreferences")
|
||||||
|
req.predicate = NSPredicate(format: "accountID = %@", account.id)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var accountID: String
|
||||||
|
@NSManaged var pinnedTimelinesData: Data?
|
||||||
|
|
||||||
|
@LazilyDecoding(from: \AccountPreferences.pinnedTimelinesData, fallback: [])
|
||||||
|
var pinnedTimelines: [Timeline]
|
||||||
|
|
||||||
|
static func `default`(account: LocalData.UserAccountInfo, context: NSManagedObjectContext) -> AccountPreferences {
|
||||||
|
let prefs = AccountPreferences(context: context)
|
||||||
|
prefs.accountID = account.id
|
||||||
|
prefs.pinnedTimelines = [.home, .public(local: true), .public(local: false)]
|
||||||
|
return prefs
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -28,6 +28,10 @@
|
||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="AccountPreferences" representedClassName="AccountPreferences" syncable="YES">
|
||||||
|
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="pinnedTimelinesData" optional="YES" attributeType="Binary"/>
|
||||||
|
</entity>
|
||||||
<entity name="Filter" representedClassName="FilterMO" syncable="YES">
|
<entity name="Filter" representedClassName="FilterMO" syncable="YES">
|
||||||
<attribute name="action" attributeType="String" defaultValueString="warn"/>
|
<attribute name="action" attributeType="String" defaultValueString="warn"/>
|
||||||
<attribute name="context" attributeType="String"/>
|
<attribute name="context" attributeType="String"/>
|
||||||
|
@ -121,6 +125,7 @@
|
||||||
<configuration name="Cloud" usedWithCloudKit="YES">
|
<configuration name="Cloud" usedWithCloudKit="YES">
|
||||||
<memberEntity name="SavedHashtag"/>
|
<memberEntity name="SavedHashtag"/>
|
||||||
<memberEntity name="SavedInstance"/>
|
<memberEntity name="SavedInstance"/>
|
||||||
|
<memberEntity name="AccountPreferences"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
<configuration name="Local">
|
<configuration name="Local">
|
||||||
<memberEntity name="Account"/>
|
<memberEntity name="Account"/>
|
||||||
|
|
|
@ -25,18 +25,22 @@ extension Timeline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var tabBarImage: UIImage? {
|
var image: UIImage {
|
||||||
switch self {
|
switch self {
|
||||||
case .home:
|
case .home:
|
||||||
return UIImage(systemName: "house.fill")
|
return UIImage(systemName: "house.fill")!
|
||||||
case let .public(local):
|
case let .public(local):
|
||||||
if local {
|
if local {
|
||||||
return UIImage(systemName: "person.and.person.fill")
|
return UIImage(systemName: "person.and.person.fill")!
|
||||||
} else {
|
} else {
|
||||||
return UIImage(systemName: "globe")
|
return UIImage(systemName: "globe")!
|
||||||
}
|
}
|
||||||
default:
|
case .list(id: _):
|
||||||
return nil
|
return UIImage(systemName: "list.bullet")!
|
||||||
|
case .tag(hashtag: _):
|
||||||
|
return UIImage(systemName: "number")!
|
||||||
|
case .direct:
|
||||||
|
return UIImage(systemName: "enveloep.fill")!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
//
|
||||||
|
// AddHashtagPinnedTimelineView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/20/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
struct AddHashtagPinnedTimelineView: View {
|
||||||
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@Binding var pinnedTimelines: [Timeline]
|
||||||
|
@StateObject private var viewModel = SearchViewModel()
|
||||||
|
@State private var searchTask: Task<Void, Never>?
|
||||||
|
@State private var isSearching = false
|
||||||
|
@State private var searchResults: [String] = []
|
||||||
|
|
||||||
|
private var savedAndFollowedHashtags: [String] {
|
||||||
|
var tags = Set<String>()
|
||||||
|
let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
|
||||||
|
for saved in (try? mastodonController.persistentContainer.viewContext.fetch(req)) ?? [] {
|
||||||
|
tags.insert(saved.name)
|
||||||
|
}
|
||||||
|
for followed in mastodonController.followedHashtags {
|
||||||
|
tags.insert(followed.name)
|
||||||
|
}
|
||||||
|
return Array(tags).sorted(using: SemiCaseSensitiveComparator())
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
list
|
||||||
|
.navigationTitle("Search")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.searchable(text: $viewModel.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Search for hashtags"))
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationViewStyle(.stack)
|
||||||
|
.onReceive(viewModel.$searchQuery, perform: { newValue in
|
||||||
|
isSearching = !newValue.isEmpty
|
||||||
|
})
|
||||||
|
.onReceive(viewModel.$searchQuery.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main), perform: { _ in
|
||||||
|
searchTask?.cancel()
|
||||||
|
searchTask = Task {
|
||||||
|
try? await updateSearchResults()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private var list: some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
if viewModel.searchQuery.isEmpty {
|
||||||
|
forEachTag(savedAndFollowedHashtags)
|
||||||
|
} else {
|
||||||
|
forEachTag(searchResults)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.opacity(isSearching ? 1 : 0)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.listRowBackground(EmptyView())
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.grouped)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func forEachTag(_ tags: [String]) -> some View {
|
||||||
|
ForEach(tags, id: \.self) { tag in
|
||||||
|
Button {
|
||||||
|
pinnedTimelines.append(.tag(hashtag: tag))
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Text("#\(tag)")
|
||||||
|
}
|
||||||
|
.tint(.primary)
|
||||||
|
.disabled(pinnedTimelines.contains(.tag(hashtag: tag)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateSearchResults() async throws {
|
||||||
|
guard !viewModel.searchQuery.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isSearching = true
|
||||||
|
let req = Client.search(query: viewModel.searchQuery, types: [.hashtags])
|
||||||
|
let (results, _) = try await mastodonController.run(req)
|
||||||
|
searchResults = results.hashtags.map(\.name)
|
||||||
|
isSearching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SearchViewModel: ObservableObject {
|
||||||
|
@Published var searchQuery = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
//struct AddHashtagPinnedTimelineView_Previews: PreviewProvider {
|
||||||
|
// static var previews: some View {
|
||||||
|
// AddHashtagPinnedTimelineView()
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// FiltersView.swift
|
// CustomizeTimelinesView.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 11/30/22.
|
// Created by Shadowfacts on 11/30/22.
|
||||||
|
@ -9,18 +9,18 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
struct FiltersView: View {
|
struct CustomizeTimelinesView: View {
|
||||||
let mastodonController: MastodonController
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
FiltersList()
|
CustomizeTimelinesList()
|
||||||
.environmentObject(mastodonController)
|
.environmentObject(mastodonController)
|
||||||
.environment(\.managedObjectContext, mastodonController.persistentContainer.viewContext)
|
.environment(\.managedObjectContext, mastodonController.persistentContainer.viewContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FiltersList: View {
|
struct CustomizeTimelinesList: View {
|
||||||
@EnvironmentObject private var mastodonController: MastodonController
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
@ObservedObject private var preferences = Preferences.shared
|
||||||
@FetchRequest(sortDescriptors: []) private var filters: FetchedResults<FilterMO>
|
@FetchRequest(sortDescriptors: []) private var filters: FetchedResults<FilterMO>
|
||||||
|
@ -50,6 +50,8 @@ struct FiltersList: View {
|
||||||
|
|
||||||
private var navigationBody: some View {
|
private var navigationBody: some View {
|
||||||
List {
|
List {
|
||||||
|
PinnedTimelinesView(accountPreferences: mastodonController.accountPreferences)
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Toggle(isOn: $preferences.hideReblogsInTimelines) {
|
Toggle(isOn: $preferences.hideReblogsInTimelines) {
|
||||||
Text("Hide Reblogs")
|
Text("Hide Reblogs")
|
||||||
|
@ -62,18 +64,27 @@ struct FiltersList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
filtersForEach(unexpiredFilters)
|
||||||
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
EditFilterView(filter: EditedFilter(), create: true, originallyExpired: false)
|
EditFilterView(filter: EditedFilter(), create: true, originallyExpired: false)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Add Filter", systemImage: "plus")
|
Label("Add Filter", systemImage: "plus")
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Active Filters")
|
||||||
}
|
}
|
||||||
|
|
||||||
filtersSection(unexpiredFilters, header: Text("Active"))
|
if !expiredFilters.isEmpty {
|
||||||
filtersSection(expiredFilters, header: Text("Expired"))
|
Section {
|
||||||
|
filtersForEach(expiredFilters)
|
||||||
|
} header: {
|
||||||
|
Text("Expired Filters")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(Text("Filters"))
|
.navigationTitle(Text("Customize Timelines"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
@ -95,30 +106,26 @@ struct FiltersList: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func filtersSection(_ filters: [FilterMO], header: some View) -> some View {
|
private func filtersForEach(_ filters: [FilterMO]) -> some View {
|
||||||
if !filters.isEmpty {
|
if !filters.isEmpty {
|
||||||
Section {
|
ForEach(filters, id: \.id) { filter in
|
||||||
ForEach(filters, id: \.id) { filter in
|
NavigationLink {
|
||||||
NavigationLink {
|
EditFilterView(filter: EditedFilter(filter), create: false, originallyExpired: filter.expiresAt != nil && filter.expiresAt! <= Date())
|
||||||
EditFilterView(filter: EditedFilter(filter), create: false, originallyExpired: filter.expiresAt != nil && filter.expiresAt! <= Date())
|
} label: {
|
||||||
} label: {
|
FilterRow(filter: filter)
|
||||||
FilterRow(filter: filter)
|
|
||||||
}
|
|
||||||
.contextMenu {
|
|
||||||
Button(role: .destructive) {
|
|
||||||
deleteFilter(filter)
|
|
||||||
} label: {
|
|
||||||
Label("Delete Filter", systemImage: "trash")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onDelete { indices in
|
.contextMenu {
|
||||||
for filter in indices.map({ filters[$0] }) {
|
Button(role: .destructive) {
|
||||||
deleteFilter(filter)
|
deleteFilter(filter)
|
||||||
|
} label: {
|
||||||
|
Label("Delete Filter", systemImage: "trash")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} header: {
|
}
|
||||||
header
|
.onDelete { indices in
|
||||||
|
for filter in indices.map({ filters[$0] }) {
|
||||||
|
deleteFilter(filter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -108,7 +108,6 @@ struct EditFilterView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Toggle("Expires", isOn: expires)
|
Toggle("Expires", isOn: expires)
|
||||||
|
|
||||||
if expires.wrappedValue {
|
if expires.wrappedValue {
|
||||||
|
@ -143,7 +142,7 @@ struct EditFilterView: View {
|
||||||
Text("Contexts")
|
Text("Contexts")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Edit Filter")
|
.navigationTitle(create ? "Add Filter" : "Edit Filter")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
@ -151,7 +150,7 @@ struct EditFilterView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
} else {
|
} else {
|
||||||
Button(create ? "Create" : "Save") {
|
Button("Save") {
|
||||||
saveFilter()
|
saveFilter()
|
||||||
}
|
}
|
||||||
.disabled(!filter.isValid(for: mastodonController) || (!edited && originallyExpired))
|
.disabled(!filter.isValid(for: mastodonController) || (!edited && originallyExpired))
|
|
@ -0,0 +1,130 @@
|
||||||
|
//
|
||||||
|
// PinnedTimelinesView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/20/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
struct PinnedTimelinesView: View {
|
||||||
|
@EnvironmentObject private var mastodonController: MastodonController
|
||||||
|
@ObservedObject private var accountPreferences: AccountPreferences
|
||||||
|
|
||||||
|
@State private var isShowingAddHashtagSheet = false
|
||||||
|
// store this separately from AccountPreferences in the view, b/c the @LazilyDecoding wrapper breaks animations
|
||||||
|
@State private var pinnedTimelines: [Timeline]
|
||||||
|
|
||||||
|
init(accountPreferences: AccountPreferences) {
|
||||||
|
self.accountPreferences = accountPreferences
|
||||||
|
self.pinnedTimelines = accountPreferences.pinnedTimelines
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
ForEach(pinnedTimelines, id: \.id) { timeline in
|
||||||
|
HStack {
|
||||||
|
Label {
|
||||||
|
if case .list(id: let id) = timeline,
|
||||||
|
let list = mastodonController.lists.first(where: { $0.id == id }) {
|
||||||
|
Text(list.title)
|
||||||
|
} else if case .tag(hashtag: let tag) = timeline {
|
||||||
|
Text(tag)
|
||||||
|
} else {
|
||||||
|
Text(timeline.title)
|
||||||
|
}
|
||||||
|
} icon: {
|
||||||
|
Image(uiImage: timeline.image.withRenderingMode(.alwaysTemplate))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "line.3.horizontal")
|
||||||
|
.foregroundColor(Color(.lightGray))
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onMove { indices, newOffset in
|
||||||
|
pinnedTimelines.move(fromOffsets: indices, toOffset: newOffset)
|
||||||
|
}
|
||||||
|
.onDelete { indices in
|
||||||
|
pinnedTimelines.remove(atOffsets: indices)
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
ForEach([Timeline.home, .public(local: true), .public(local: false)], id: \.id) { timeline in
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
pinnedTimelines.append(timeline)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label {
|
||||||
|
Text(timeline.title)
|
||||||
|
} icon: {
|
||||||
|
Image(uiImage: timeline.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(pinnedTimelines.contains(timeline))
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu("List…") {
|
||||||
|
ForEach(mastodonController.lists, id: \.id) { list in
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
pinnedTimelines.append(list.timeline)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text(list.title)
|
||||||
|
}
|
||||||
|
.disabled(pinnedTimelines.contains(list.timeline))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
isShowingAddHashtagSheet = true
|
||||||
|
} label: {
|
||||||
|
Label("Hashtag…", systemImage: "number")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Add…", systemImage: "plus")
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
} header: {
|
||||||
|
Text("Pinned Timelines")
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
|
||||||
|
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||||
|
})
|
||||||
|
.onReceive(accountPreferences.publisher(for: \.pinnedTimelinesData)) { _ in
|
||||||
|
if pinnedTimelines != accountPreferences.pinnedTimelines {
|
||||||
|
pinnedTimelines = accountPreferences.pinnedTimelines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: pinnedTimelines) { newValue in
|
||||||
|
if accountPreferences.pinnedTimelines != newValue {
|
||||||
|
accountPreferences.pinnedTimelines = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension Timeline {
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .home:
|
||||||
|
return "home"
|
||||||
|
case .public(local: let local):
|
||||||
|
return "public:\(local)"
|
||||||
|
case .list(id: let id):
|
||||||
|
return "list:\(id)"
|
||||||
|
case .tag(hashtag: let tag):
|
||||||
|
return "tag:\(tag)"
|
||||||
|
case .direct:
|
||||||
|
return "direct"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,9 +11,6 @@ import Pachyderm
|
||||||
|
|
||||||
class NotificationsPageViewController: SegmentedPageViewController<NotificationsPageViewController.Page> {
|
class NotificationsPageViewController: SegmentedPageViewController<NotificationsPageViewController.Page> {
|
||||||
|
|
||||||
private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title")
|
|
||||||
private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title")
|
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
var initialMode: NotificationsMode?
|
var initialMode: NotificationsMode?
|
||||||
|
@ -22,20 +19,14 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
|
||||||
self.initialMode = initialMode
|
self.initialMode = initialMode
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases, mastodonController: mastodonController)
|
super.init(pages: [.all, .mentions]) { page in
|
||||||
notifications.title = notificationsTitle
|
let vc = NotificationsTableViewController(allowedTypes: page.allowedTypes, mastodonController: mastodonController)
|
||||||
notifications.userActivity = UserActivityManager.checkNotificationsActivity(mode: .allNotifications)
|
vc.title = page.title
|
||||||
|
vc.userActivity = page.userActivity
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
|
||||||
let mentions = NotificationsTableViewController(allowedTypes: [.mention], mastodonController: mastodonController)
|
title = Page.all.title
|
||||||
mentions.title = mentionsTitle
|
|
||||||
mentions.userActivity = UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly)
|
|
||||||
|
|
||||||
super.init(pages: [
|
|
||||||
(.all, notificationsTitle, notifications),
|
|
||||||
(.mentions, mentionsTitle, mentions),
|
|
||||||
])
|
|
||||||
|
|
||||||
title = notificationsTitle
|
|
||||||
tabBarItem.image = UIImage(systemName: "bell.fill")
|
tabBarItem.image = UIImage(systemName: "bell.fill")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,9 +52,40 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
|
||||||
selectPage(page, animated: false)
|
selectPage(page, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Page {
|
enum Page: SegmentedPageViewControllerPage {
|
||||||
case all
|
case all
|
||||||
case mentions
|
case mentions
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .all:
|
||||||
|
return NSLocalizedString("Notifications", comment: "notifications tab title")
|
||||||
|
case .mentions:
|
||||||
|
return NSLocalizedString("Mentions", comment: "mentions tab title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentedControlTitle: String {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedTypes: [Pachyderm.Notification.Kind] {
|
||||||
|
switch self {
|
||||||
|
case .all:
|
||||||
|
return Pachyderm.Notification.Kind.allCases
|
||||||
|
case .mentions:
|
||||||
|
return [.mention]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userActivity: NSUserActivity {
|
||||||
|
switch self {
|
||||||
|
case .all:
|
||||||
|
return UserActivityManager.checkNotificationsActivity(mode: .allNotifications)
|
||||||
|
case .mentions:
|
||||||
|
return UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageViewController.Page> {
|
class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageViewController.Page> {
|
||||||
|
|
||||||
|
@ -17,33 +18,27 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
private var pinnedTimelinesObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
let pages = mastodonController.accountPreferences.pinnedTimelines.map {
|
||||||
home.title = homeTitle
|
Page(mastodonController: mastodonController, timeline: $0)
|
||||||
home.persistsState = true
|
}
|
||||||
|
super.init(pages: pages) { page in
|
||||||
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
|
let vc = TimelineViewController(for: page.timeline, mastodonController: page.mastodonController)
|
||||||
federated.title = federatedTitle
|
vc.title = page.segmentedControlTitle
|
||||||
federated.persistsState = true
|
vc.persistsState = true
|
||||||
|
return vc
|
||||||
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
|
}
|
||||||
local.title = localTitle
|
|
||||||
local.persistsState = true
|
|
||||||
|
|
||||||
super.init(pages: [
|
|
||||||
(.home, "Home", home),
|
|
||||||
(.local, "Local", local),
|
|
||||||
(.federated, "Federated", federated),
|
|
||||||
])
|
|
||||||
|
|
||||||
title = homeTitle
|
title = homeTitle
|
||||||
tabBarItem.image = UIImage(systemName: "house.fill")
|
tabBarItem.image = UIImage(systemName: "house.fill")
|
||||||
|
|
||||||
let filtersItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), style: .plain, target: self, action: #selector(filtersPressed))
|
let customizeItem = UIBarButtonItem(image: UIImage(systemName: "slider.horizontal.3"), style: .plain, target: self, action: #selector(customizePressed))
|
||||||
filtersItem.accessibilityLabel = "Filters"
|
customizeItem.accessibilityLabel = "Customize Timelines"
|
||||||
navigationItem.leftBarButtonItem = filtersItem
|
navigationItem.rightBarButtonItem = customizeItem
|
||||||
|
|
||||||
let jumpToPresentName = NSMutableAttributedString("Jump to Present")
|
let jumpToPresentName = NSMutableAttributedString("Jump to Present")
|
||||||
// otherwise it pronounces it as 'pɹizˈənt'
|
// otherwise it pronounces it as 'pɹizˈənt'
|
||||||
|
@ -51,7 +46,7 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
jumpToPresentName.addAttribute(.accessibilitySpeechIPANotation, value: "ˈprɛ.zənt", range: NSRange(location: "Jump to ".count, length: "Present".count))
|
jumpToPresentName.addAttribute(.accessibilitySpeechIPANotation, value: "ˈprɛ.zənt", range: NSRange(location: "Jump to ".count, length: "Present".count))
|
||||||
segmentedControl.accessibilityCustomActions = [
|
segmentedControl.accessibilityCustomActions = [
|
||||||
UIAccessibilityCustomAction(attributedName: jumpToPresentName, actionHandler: { [unowned self] _ in
|
UIAccessibilityCustomAction(attributedName: jumpToPresentName, actionHandler: { [unowned self] _ in
|
||||||
guard let vc = pageControllers[currentIndex] as? TimelineViewController else {
|
guard let vc = currentViewController as? TimelineViewController else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Task {
|
Task {
|
||||||
|
@ -60,42 +55,61 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
|
pinnedTimelinesObservation = mastodonController.accountPreferences.observe(\.pinnedTimelinesData, changeHandler: { [unowned self] _, _ in
|
||||||
|
let pages = self.mastodonController.accountPreferences.pinnedTimelines.map {
|
||||||
|
Page(mastodonController: self.mastodonController, timeline: $0)
|
||||||
|
}
|
||||||
|
self.setPages(pages, animated: false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectTimeline(_ timeline: Timeline, animated: Bool) {
|
||||||
|
self.selectPage(Page(mastodonController: mastodonController, timeline: timeline), animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
func stateRestorationActivity() -> NSUserActivity? {
|
func stateRestorationActivity() -> NSUserActivity? {
|
||||||
return (pageControllers[currentIndex] as! TimelineViewController).stateRestorationActivity()
|
return (currentViewController as! TimelineViewController).stateRestorationActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreActivity(_ activity: NSUserActivity) {
|
func restoreActivity(_ activity: NSUserActivity) {
|
||||||
guard let timeline = UserActivityManager.getTimeline(from: activity) else {
|
guard let timeline = UserActivityManager.getTimeline(from: activity) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let page: Page
|
let page = Page(mastodonController: mastodonController, timeline: timeline)
|
||||||
switch timeline {
|
|
||||||
case .home:
|
|
||||||
page = .home
|
|
||||||
case .public(local: false):
|
|
||||||
page = .federated
|
|
||||||
case .public(local: true):
|
|
||||||
page = .local
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
selectPage(page, animated: false)
|
selectPage(page, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func filtersPressed() {
|
@objc private func customizePressed() {
|
||||||
present(UIHostingController(rootView: FiltersView(mastodonController: mastodonController)), animated: true)
|
present(UIHostingController(rootView: CustomizeTimelinesView(mastodonController: mastodonController)), animated: true)
|
||||||
}
|
|
||||||
|
|
||||||
enum Page: Hashable {
|
|
||||||
case home
|
|
||||||
case local
|
|
||||||
case federated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimelinesPageViewController {
|
||||||
|
struct Page: SegmentedPageViewControllerPage {
|
||||||
|
let mastodonController: MastodonController
|
||||||
|
let timeline: Timeline
|
||||||
|
|
||||||
|
static func ==(lhs: Page, rhs: Page) -> Bool {
|
||||||
|
return lhs.timeline == rhs.timeline
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(timeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentedControlTitle: String {
|
||||||
|
if case let .list(id) = timeline,
|
||||||
|
let list = try? mastodonController.persistentContainer.viewContext.fetch(ListMO.fetchRequest(id: id)).first {
|
||||||
|
return list.title
|
||||||
|
} else {
|
||||||
|
return timeline.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,35 +8,39 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageViewControllerDelegate, TabbedPageViewController {
|
protocol SegmentedPageViewControllerPage: Hashable {
|
||||||
|
var segmentedControlTitle: String { get }
|
||||||
|
}
|
||||||
|
|
||||||
let pages: [Page]
|
class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIPageViewController, UIPageViewControllerDelegate, TabbedPageViewController {
|
||||||
let pageControllers: [UIViewController]
|
|
||||||
|
private(set) var pages: [Page]!
|
||||||
|
private let pageProvider: (Page) -> UIViewController
|
||||||
|
private var pageControllers = [Page: UIViewController]()
|
||||||
|
|
||||||
private var initialPage: Page
|
private var initialPage: Page
|
||||||
private var currentPage: Page
|
private var currentPage: Page
|
||||||
var currentIndex: Int {
|
var currentIndex: Int! {
|
||||||
pages.firstIndex(of: currentPage)!
|
pages.firstIndex(of: currentPage)
|
||||||
|
}
|
||||||
|
var currentViewController: UIViewController {
|
||||||
|
viewControllers!.first!
|
||||||
}
|
}
|
||||||
|
|
||||||
let segmentedControl = ScrollingSegmentedControl<Page>()
|
let segmentedControl = ScrollingSegmentedControl<Page>()
|
||||||
|
|
||||||
init(pages: [(Page, String, UIViewController)]) {
|
init(pages: [Page], pageProvider: @escaping (Page) -> UIViewController) {
|
||||||
precondition(!pages.isEmpty)
|
precondition(!pages.isEmpty)
|
||||||
|
|
||||||
self.pages = pages.map(\.0)
|
self.pageProvider = pageProvider
|
||||||
self.pageControllers = pages.map(\.2)
|
|
||||||
|
|
||||||
initialPage = self.pages.first!
|
initialPage = pages.first!
|
||||||
currentPage = self.pages.first!
|
currentPage = pages.first!
|
||||||
|
|
||||||
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||||
|
|
||||||
// this needs to happen in init because EnhancedNavigationViewController expects to be able to look at the titleView
|
setPages(pages, animated: false)
|
||||||
// before the view has necessarily loaded
|
|
||||||
segmentedControl.options = pages.map {
|
|
||||||
.init(value: $0.0, name: $0.1)
|
|
||||||
}
|
|
||||||
segmentedControl.didSelectOption = { [unowned self] option in
|
segmentedControl.didSelectOption = { [unowned self] option in
|
||||||
if let option {
|
if let option {
|
||||||
self.selectPage(option, animated: true)
|
self.selectPage(option, animated: true)
|
||||||
|
@ -54,6 +58,26 @@ class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageV
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setPages(_ pages: [Page], animated: Bool) {
|
||||||
|
precondition(!pages.isEmpty)
|
||||||
|
|
||||||
|
self.pages = pages
|
||||||
|
|
||||||
|
if !pages.contains(currentPage) {
|
||||||
|
selectPage(pages.first!, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in pageControllers.keys where !pages.contains(key) {
|
||||||
|
pageControllers.removeValue(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this needs to happen in init because EnhancedNavigationViewController expects to be able to look at the titleView
|
||||||
|
// before the view has necessarily loaded
|
||||||
|
segmentedControl.options = pages.map {
|
||||||
|
.init(value: $0, name: $0.segmentedControlTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -80,12 +104,23 @@ class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageV
|
||||||
initialPage = page
|
initialPage = page
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let prevIndex = currentIndex
|
let direction: UIPageViewController.NavigationDirection
|
||||||
currentPage = page
|
if let prevIndex = currentIndex {
|
||||||
let index = pages.firstIndex(of: page)!
|
let index = pages.firstIndex(of: page)!
|
||||||
let newController = pageControllers[index]
|
direction = index - prevIndex > 0 ? .forward : .reverse
|
||||||
|
} else {
|
||||||
|
direction = .forward
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage = page
|
||||||
|
let newController: UIViewController
|
||||||
|
if let existing = pageControllers[page] {
|
||||||
|
newController = existing
|
||||||
|
} else {
|
||||||
|
newController = pageProvider(page)
|
||||||
|
pageControllers[page] = newController
|
||||||
|
}
|
||||||
|
|
||||||
let direction: UIPageViewController.NavigationDirection = index - prevIndex > 0 ? .forward : .reverse
|
|
||||||
setViewControllers([newController], direction: direction, animated: animated)
|
setViewControllers([newController], direction: direction, animated: animated)
|
||||||
navigationItem.title = newController.title
|
navigationItem.title = newController.title
|
||||||
|
|
||||||
|
@ -108,7 +143,7 @@ class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageV
|
||||||
|
|
||||||
extension SegmentedPageViewController: TabBarScrollableViewController {
|
extension SegmentedPageViewController: TabBarScrollableViewController {
|
||||||
func tabBarScrollToTop() {
|
func tabBarScrollToTop() {
|
||||||
if let scrollableVC = pageControllers[currentIndex] as? TabBarScrollableViewController {
|
if let scrollableVC = currentViewController as? TabBarScrollableViewController {
|
||||||
scrollableVC.tabBarScrollToTop()
|
scrollableVC.tabBarScrollToTop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +151,7 @@ extension SegmentedPageViewController: TabBarScrollableViewController {
|
||||||
|
|
||||||
extension SegmentedPageViewController: BackgroundableViewController {
|
extension SegmentedPageViewController: BackgroundableViewController {
|
||||||
func sceneDidEnterBackground() {
|
func sceneDidEnterBackground() {
|
||||||
if let current = pageControllers[currentIndex] as? BackgroundableViewController {
|
if let current = currentViewController as? BackgroundableViewController {
|
||||||
current.sceneDidEnterBackground()
|
current.sceneDidEnterBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +159,7 @@ extension SegmentedPageViewController: BackgroundableViewController {
|
||||||
|
|
||||||
extension SegmentedPageViewController: StatusBarTappableViewController {
|
extension SegmentedPageViewController: StatusBarTappableViewController {
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
if let current = pageControllers[currentIndex] as? StatusBarTappableViewController {
|
if let current = currentViewController as? StatusBarTappableViewController {
|
||||||
return current.handleStatusBarTapped(xPosition: xPosition)
|
return current.handleStatusBarTapped(xPosition: xPosition)
|
||||||
}
|
}
|
||||||
return .continue
|
return .continue
|
||||||
|
|
|
@ -216,23 +216,11 @@ class UserActivityManager {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch timeline {
|
if mastodonController.accountPreferences.pinnedTimelines.contains(timeline) {
|
||||||
case .home, .public(true), .public(false):
|
|
||||||
navigationController.popToRootViewController(animated: false)
|
navigationController.popToRootViewController(animated: false)
|
||||||
let rootController = navigationController.viewControllers.first! as! TimelinesPageViewController
|
let rootController = navigationController.viewControllers.first! as! TimelinesPageViewController
|
||||||
let page: TimelinesPageViewController.Page
|
rootController.selectTimeline(timeline, animated: false)
|
||||||
switch timeline {
|
} else {
|
||||||
case .home:
|
|
||||||
page = .home
|
|
||||||
case .public(local: false):
|
|
||||||
page = .federated
|
|
||||||
case .public(local: true):
|
|
||||||
page = .local
|
|
||||||
default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
rootController.selectPage(page, animated: false)
|
|
||||||
default:
|
|
||||||
let timeline = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
let timeline = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||||
navigationController.pushViewController(timeline, animated: false)
|
navigationController.pushViewController(timeline, animated: false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,9 @@ class ScrollingSegmentedControl<Value: Hashable>: UIScrollView, UIGestureRecogni
|
||||||
label.accessibilityLabel = "\(option.name), \(index + 1) of \(options.count)"
|
label.accessibilityLabel = "\(option.name), \(index + 1) of \(options.count)"
|
||||||
optionsStack.addArrangedSubview(label)
|
optionsStack.addArrangedSubview(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSelectedIndicatorView()
|
||||||
|
invalidateIntrinsicContentSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSelectedOption(_ value: Value, animated: Bool) {
|
func setSelectedOption(_ value: Value, animated: Bool) {
|
||||||
|
|
Loading…
Reference in New Issue