Compare commits

..

3 Commits

10 changed files with 193 additions and 47 deletions

View File

@ -199,8 +199,10 @@ public class Client {
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials") return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
} }
public static func getFavourites() -> Request<[Status]> { public static func getFavourites(range: RequestRange = .default) -> Request<[Status]> {
return Request<[Status]>(method: .get, path: "/api/v1/favourites") var request = Request<[Status]>(method: .get, path: "/api/v1/favourites")
request.range = range
return request
} }
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> { public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {

View File

@ -295,6 +295,8 @@
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; }; D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; }; D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */; };
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */; };
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; }; D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; }; D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
@ -321,6 +323,7 @@
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; }; D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; }; D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; }; D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; }; D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; }; D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; }; D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
@ -701,6 +704,8 @@
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; }; D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; }; D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPredicateStatusesViewController.swift; sourceTree = "<group>"; };
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = "<group>"; };
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; }; D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; }; D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
@ -734,6 +739,7 @@
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; }; D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; }; D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
D6D706A829498C82000827ED /* Tusker.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tusker.xcconfig; sourceTree = "<group>"; }; D6D706A829498C82000827ED /* Tusker.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Tusker.xcconfig; sourceTree = "<group>"; };
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; }; D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; }; D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; }; D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
@ -935,12 +941,14 @@
path = Explore; path = Explore;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D627944823A6AD5100D38C68 /* Bookmarks */ = { D627944823A6AD5100D38C68 /* Local Predicate Statuses List */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6C3F4F4298ED0890009FCFF /* LocalPredicateStatusesViewController.swift */,
D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */, D6DD8FFE2984D327002AD3FD /* BookmarksViewController.swift */,
D6C3F4F6298ED7F70009FCFF /* FavoritesViewController.swift */,
); );
path = Bookmarks; path = "Local Predicate Statuses List";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D627944B23A9A02400D38C68 /* Lists */ = { D627944B23A9A02400D38C68 /* Lists */ = {
@ -1003,7 +1011,6 @@
D6A3BC822321F69400FD64D5 /* Account List */, D6A3BC822321F69400FD64D5 /* Account List */,
D6B053A023BD2BED00A066FA /* Asset Picker */, D6B053A023BD2BED00A066FA /* Asset Picker */,
0411610522B457290030A9B7 /* Attachment Gallery */, 0411610522B457290030A9B7 /* Attachment Gallery */,
D627944823A6AD5100D38C68 /* Bookmarks */,
D641C787213DD862004B4513 /* Compose */, D641C787213DD862004B4513 /* Compose */,
D641C785213DD83B004B4513 /* Conversation */, D641C785213DD83B004B4513 /* Conversation */,
D6F2E960249E772F005846BB /* Crash Reporter */, D6F2E960249E772F005846BB /* Crash Reporter */,
@ -1012,6 +1019,7 @@
D61F759729384D4200C0B37F /* Customize Timelines */, D61F759729384D4200C0B37F /* Customize Timelines */,
D641C788213DD86D004B4513 /* Large Image */, D641C788213DD86D004B4513 /* Large Image */,
D627944B23A9A02400D38C68 /* Lists */, D627944B23A9A02400D38C68 /* Lists */,
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */,
D641C782213DD7F0004B4513 /* Main */, D641C782213DD7F0004B4513 /* Main */,
D6F6A555291F4F0C00F496A8 /* Mute */, D6F6A555291F4F0C00F496A8 /* Mute */,
D641C786213DD852004B4513 /* Notifications */, D641C786213DD852004B4513 /* Notifications */,
@ -1414,6 +1422,7 @@
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */, D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */,
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */, D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
D620483523D38075008A63EF /* ContentTextView.swift */, D620483523D38075008A63EF /* ContentTextView.swift */,
D6D9498E298EB79400C59229 /* CopyableLable.swift */,
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */, D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
D6969E9F240C8384002843CE /* EmojiLabel.swift */, D6969E9F240C8384002843CE /* EmojiLabel.swift */,
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */, D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */,
@ -1936,6 +1945,7 @@
D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */,
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */, D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */, D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */, D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */, D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
@ -2117,6 +2127,7 @@
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */, D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */, D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */,
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */, D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */, D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
@ -2147,6 +2158,7 @@
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */, D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */, D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */, D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
D6C3F4F7298ED7F70009FCFF /* FavoritesViewController.swift in Sources */,
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */, D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,

View File

@ -154,7 +154,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
private func applyInitialSnapshot() { private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Section.allCases.filter { $0 != .discover }) snapshot.appendSections(Section.allCases.filter { $0 != .discover })
snapshot.appendItems([.bookmarks], toSection: .bookmarks) snapshot.appendItems([.bookmarks, .favorites], toSection: .bookmarks)
if mastodonController.instanceFeatures.trends, if mastodonController.instanceFeatures.trends,
!Preferences.shared.hideDiscover { !Preferences.shared.hideDiscover {
addDiscoverSection(to: &snapshot) addDiscoverSection(to: &snapshot)
@ -336,6 +336,9 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
case .bookmarks: case .bookmarks:
show(BookmarksViewController(mastodonController: mastodonController), sender: nil) show(BookmarksViewController(mastodonController: mastodonController), sender: nil)
case .favorites:
show(FavoritesViewController(mastodonController: mastodonController), sender: nil)
case .trendingStatuses: case .trendingStatuses:
show(TrendingStatusesViewController(mastodonController: mastodonController), sender: nil) show(TrendingStatusesViewController(mastodonController: mastodonController), sender: nil)
@ -408,6 +411,7 @@ extension ExploreViewController {
enum Item: Hashable { enum Item: Hashable {
case bookmarks case bookmarks
case favorites
case trendingStatuses case trendingStatuses
case trendingTags case trendingTags
case trendingLinks case trendingLinks
@ -423,6 +427,8 @@ extension ExploreViewController {
switch self { switch self {
case .bookmarks: case .bookmarks:
return NSLocalizedString("Bookmarks", comment: "bookmarks nav item title") return NSLocalizedString("Bookmarks", comment: "bookmarks nav item title")
case .favorites:
return NSLocalizedString("Favorites", comment: "favorites nav item title")
case .trendingStatuses: case .trendingStatuses:
return NSLocalizedString("Trending Posts", comment: "trending statuses nav item title") return NSLocalizedString("Trending Posts", comment: "trending statuses nav item title")
case .trendingTags: case .trendingTags:
@ -451,6 +457,8 @@ extension ExploreViewController {
switch self { switch self {
case .bookmarks: case .bookmarks:
name = "bookmark.fill" name = "bookmark.fill"
case .favorites:
name = "star.fill"
case .trendingStatuses: case .trendingStatuses:
name = "doc.text.image" name = "doc.text.image"
case .trendingTags: case .trendingTags:
@ -477,6 +485,8 @@ extension ExploreViewController {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.bookmarks, .bookmarks): case (.bookmarks, .bookmarks):
return true return true
case (.favorites, .favorites):
return true
case (.trendingStatuses, .trendingStatuses): case (.trendingStatuses, .trendingStatuses):
return true return true
case (.trendingTags, .trendingTags): case (.trendingTags, .trendingTags):
@ -506,6 +516,8 @@ extension ExploreViewController {
switch self { switch self {
case .bookmarks: case .bookmarks:
hasher.combine("bookmarks") hasher.combine("bookmarks")
case .favorites:
hasher.combine("favorites")
case .trendingStatuses: case .trendingStatuses:
hasher.combine("trendingStatuses") hasher.combine("trendingStatuses")
case .trendingTags: case .trendingTags:

View File

@ -0,0 +1,27 @@
//
// BookmarksViewController.swift
// Tusker
//
// Created by Shadowfacts on 12/15/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class BookmarksViewController: LocalPredicateStatusesViewController {
init(mastodonController: MastodonController) {
super.init(
predicate: { $0.bookmarked ?? false },
predicateTitle: "Bookmarks",
request: { Client.getBookmarks(range: $0) },
mastodonController: mastodonController
)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,27 @@
//
// FavoritesViewController.swift
// Tusker
//
// Created by Shadowfacts on 2/4/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class FavoritesViewController: LocalPredicateStatusesViewController {
init(mastodonController: MastodonController) {
super.init(
predicate: \.favourited,
predicateTitle: "Favorites",
request: { Client.getFavourites(range: $0) },
mastodonController: mastodonController
)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,20 +1,23 @@
// //
// BookmarksViewController.swift // LocalPredicateStatusesViewController.swift
// Tusker // Tusker
// //
// Created by Shadowfacts on 12/15/19. // Created by Shadowfacts on 2/4/23.
// Copyright © 2019 Shadowfacts. All rights reserved. // Copyright © 2023 Shadowfacts. All rights reserved.
// //
import UIKit import UIKit
import Pachyderm import Pachyderm
import CoreData import CoreData
class BookmarksViewController: UIViewController, CollectionViewController, RefreshableViewController { class LocalPredicateStatusesViewController: UIViewController, CollectionViewController, RefreshableViewController {
private static let pageSize = 40 private static let pageSize = 40
let mastodonController: MastodonController let mastodonController: MastodonController
private let predicate: (StatusMO) -> Bool
private let predicateTitle: String
private let request: (RequestRange) -> Request<[Status]>
var collectionView: UICollectionView! { var collectionView: UICollectionView! {
view as? UICollectionView view as? UICollectionView
@ -25,12 +28,15 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
private var newer: RequestRange? private var newer: RequestRange?
private var older: RequestRange? private var older: RequestRange?
init(mastodonController: MastodonController) { init(predicate: @escaping (StatusMO) -> Bool, predicateTitle: String, request: @escaping (RequestRange) -> Request<[Status]>, mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.predicate = predicate
self.predicateTitle = predicateTitle
self.request = request
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
title = NSLocalizedString("Bookmarks", comment: "bookmarks screen title") self.title = predicateTitle
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -50,7 +56,7 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
return sectionConfig return sectionConfig
} }
var config = sectionConfig var config = sectionConfig
if item.hideIndicators { if item.hideSeparators {
config.topSeparatorVisibility = .hidden config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden config.bottomSeparatorVisibility = .hidden
} else { } else {
@ -100,7 +106,7 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
#endif #endif
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Bookmarks")) addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)"))
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext) NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext)
@ -129,12 +135,12 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
state = .loadingInitial state = .loadingInitial
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.bookmarks]) snapshot.appendSections([.statuses])
snapshot.appendItems([.loadingIndicator]) snapshot.appendItems([.loadingIndicator])
await apply(snapshot: snapshot, animatingDifferences: false) await apply(snapshot: snapshot, animatingDifferences: false)
do { do {
let req = Client.getBookmarks(range: .count(BookmarksViewController.pageSize)) let req = request(.count(Self.pageSize))
let (statuses, pagination) = try await mastodonController.run(req) let (statuses, pagination) = try await mastodonController.run(req)
newer = pagination?.newer newer = pagination?.newer
older = pagination?.older older = pagination?.older
@ -142,14 +148,14 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
await mastodonController.persistentContainer.addAll(statuses: statuses) await mastodonController.persistentContainer.addAll(statuses: statuses)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.bookmarks]) snapshot.appendSections([.statuses])
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, addedLocally: false) }) snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, addedLocally: false) })
await apply(snapshot: snapshot, animatingDifferences: true) await apply(snapshot: snapshot, animatingDifferences: true)
state = .loaded state = .loaded
} catch { } catch {
let config = ToastConfiguration(from: error, with: "Error Loading Bookmarks", in: self) { [weak self] toast in let config = ToastConfiguration(from: error, with: "Error Loading \(predicateTitle)", in: self) { [weak self] toast in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
await self?.loadInitial() await self?.loadInitial()
} }
@ -174,7 +180,7 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
await apply(snapshot: snapshot, animatingDifferences: false) await apply(snapshot: snapshot, animatingDifferences: false)
do { do {
let req = Client.getBookmarks(range: older.withCount(BookmarksViewController.pageSize)) let req = request(older.withCount(Self.pageSize))
let (statuses, pagination) = try await mastodonController.run(req) let (statuses, pagination) = try await mastodonController.run(req)
self.older = pagination?.older self.older = pagination?.older
@ -184,12 +190,13 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, addedLocally: false) }) snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown, addedLocally: false) })
await apply(snapshot: snapshot, animatingDifferences: true) await apply(snapshot: snapshot, animatingDifferences: true)
} catch { } catch {
let config = ToastConfiguration(from: error, with: "Error Loading Older Bookmarks", in: self) { [weak self] toast in let config = ToastConfiguration(from: error, with: "Error Loading Older \(predicateTitle)", in: self) { [weak self] toast in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
await self?.loadOlder() await self?.loadOlder()
} }
showToast(configuration: config, animated: true) showToast(configuration: config, animated: true)
var snapshot = dataSource.snapshot()
snapshot.deleteItems([.loadingIndicator]) snapshot.deleteItems([.loadingIndicator])
await apply(snapshot: snapshot, animatingDifferences: false) await apply(snapshot: snapshot, animatingDifferences: false)
} }
@ -234,7 +241,7 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
} }
var hasChanges = false var hasChanges = false
if let inserted = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> { if let inserted = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> {
for case let status as StatusMO in inserted where status.bookmarked == true { for case let status as StatusMO in inserted where predicate(status) {
prepend(item: .status(id: status.id, state: .unknown, addedLocally: true)) prepend(item: .status(id: status.id, state: .unknown, addedLocally: true))
hasChanges = true hasChanges = true
} }
@ -243,10 +250,10 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
for case let status as StatusMO in updated { for case let status as StatusMO in updated {
let item = Item.status(id: status.id, state: .unknown, addedLocally: true) let item = Item.status(id: status.id, state: .unknown, addedLocally: true)
let exists = snapshot.itemIdentifiers.contains(item) let exists = snapshot.itemIdentifiers.contains(item)
if status.bookmarked == true && !exists { if predicate(status) && !exists {
prepend(item: item) prepend(item: item)
hasChanges = true hasChanges = true
} else if status.bookmarked == false && exists { } else if !predicate(status) && exists {
snapshot.deleteItems([item]) snapshot.deleteItems([item])
hasChanges = true hasChanges = true
} }
@ -272,22 +279,21 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
state = .loadingNewer state = .loadingNewer
Task { Task {
do { do {
let req = Client.getBookmarks(range: newer.withCount(BookmarksViewController.pageSize)) let req = request(newer.withCount(Self.pageSize))
let (statuses, pagination) = try await mastodonController.run(req) let (statuses, pagination) = try await mastodonController.run(req)
self.newer = pagination?.newer self.newer = pagination?.newer
await mastodonController.persistentContainer.addAll(statuses: statuses) await mastodonController.persistentContainer.addAll(statuses: statuses)
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
let localItems: [String: CollapseState] = Dictionary(uniqueKeysWithValues: snapshot.itemIdentifiers.compactMap({ let localItems: [String: CollapseState] = Dictionary(uniqueKeysWithValues: snapshot.itemIdentifiers.compactMap {
if case .status(id: let id, state: let state, addedLocally: true) = $0 { if case .status(id: let id, state: let state, addedLocally: true) = $0 {
return (id, state) return (id, state)
} else { } else {
return nil return nil
} }
})) })
var newItems: [Item] = [] var newItems: [Item] = []
for status in statuses { for status in statuses {
let state: CollapseState let state: CollapseState
@ -305,8 +311,9 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
snapshot.appendItems(newItems) snapshot.appendItems(newItems)
} }
await apply(snapshot: snapshot, animatingDifferences: true) await apply(snapshot: snapshot, animatingDifferences: true)
} catch { } catch {
let config = ToastConfiguration(from: error, with: "Error Refreshing Bookmarks", in: self) { [weak self] toast in let config = ToastConfiguration(from: error, with: "Error Refreshing \(predicateTitle)", in: self) { [weak self] toast in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
self?.refresh() self?.refresh()
} }
@ -322,15 +329,15 @@ class BookmarksViewController: UIViewController, CollectionViewController, Refre
} }
extension BookmarksViewController { extension LocalPredicateStatusesViewController {
enum Section { enum Section {
case bookmarks case statuses
} }
enum Item: Equatable, Hashable { enum Item: Equatable, Hashable {
case status(id: String, state: CollapseState, addedLocally: Bool) case status(id: String, state: CollapseState, addedLocally: Bool)
case loadingIndicator case loadingIndicator
var hideIndicators: Bool { var hideSeparators: Bool {
switch self { switch self {
case .loadingIndicator: case .loadingIndicator:
return true return true
@ -362,7 +369,7 @@ extension BookmarksViewController {
} }
} }
extension BookmarksViewController { extension LocalPredicateStatusesViewController {
enum State { enum State {
case unloaded case unloaded
case loadingInitial case loadingInitial
@ -372,7 +379,7 @@ extension BookmarksViewController {
} }
} }
extension BookmarksViewController: UICollectionViewDelegate { extension LocalPredicateStatusesViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.section == 0, if indexPath.section == 0,
indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 { indexPath.row == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
@ -397,7 +404,7 @@ extension BookmarksViewController: UICollectionViewDelegate {
} }
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
(collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration() return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
} }
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
@ -405,17 +412,17 @@ extension BookmarksViewController: UICollectionViewDelegate {
} }
} }
extension BookmarksViewController: UICollectionViewDragDelegate { extension LocalPredicateStatusesViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
(collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? [] return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? []
} }
} }
extension BookmarksViewController: TuskerNavigationDelegate { extension LocalPredicateStatusesViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController } var apiController: MastodonController! { mastodonController }
} }
extension BookmarksViewController: StatusCollectionViewCellDelegate { extension LocalPredicateStatusesViewController: StatusCollectionViewCellDelegate {
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) { func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
if let indexPath = collectionView.indexPath(for: cell) { if let indexPath = collectionView.indexPath(for: cell) {
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
@ -425,17 +432,17 @@ extension BookmarksViewController: StatusCollectionViewCellDelegate {
} }
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) { func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
// bookmarks aren't filtered // filtering isn't supported here
} }
} }
extension BookmarksViewController: TabBarScrollableViewController { extension LocalPredicateStatusesViewController: TabBarScrollableViewController {
func tabBarScrollToTop() { func tabBarScrollToTop() {
collectionView.scrollToTop() collectionView.scrollToTop()
} }
} }
extension BookmarksViewController: StatusBarTappableViewController { extension LocalPredicateStatusesViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop

View File

@ -41,7 +41,7 @@ class MainSidebarViewController: UIViewController {
} }
var exploreTabItems: [Item] { var exploreTabItems: [Item] {
var items: [Item] = [.explore, .bookmarks, .profileDirectory] var items: [Item] = [.explore, .bookmarks, .favorites, .profileDirectory]
let snapshot = dataSource.snapshot() let snapshot = dataSource.snapshot()
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) { for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
items.append(.list(list)) items.append(.list(list))
@ -172,6 +172,7 @@ class MainSidebarViewController: UIViewController {
.tab(.notifications), .tab(.notifications),
.explore, .explore,
.bookmarks, .bookmarks,
.favorites,
.tab(.myProfile) .tab(.myProfile)
], toSection: .tabs) ], toSection: .tabs)
snapshot.appendItems([ snapshot.appendItems([
@ -384,7 +385,7 @@ extension MainSidebarViewController {
} }
enum Item: Hashable { enum Item: Hashable {
case tab(MainTabBarViewController.Tab) case tab(MainTabBarViewController.Tab)
case explore, bookmarks case explore, bookmarks, favorites
case discoverHeader, profileDirectory case discoverHeader, profileDirectory
case listsHeader, list(List), addList case listsHeader, list(List), addList
case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag case savedHashtagsHeader, savedHashtag(Hashtag), addSavedHashtag
@ -398,6 +399,8 @@ extension MainSidebarViewController {
return "Explore" return "Explore"
case .bookmarks: case .bookmarks:
return "Bookmarks" return "Bookmarks"
case .favorites:
return "Favorites"
case .discoverHeader: case .discoverHeader:
return "Discover" return "Discover"
case .profileDirectory: case .profileDirectory:
@ -431,6 +434,8 @@ extension MainSidebarViewController {
return "magnifyingglass" return "magnifyingglass"
case .bookmarks: case .bookmarks:
return "bookmark" return "bookmark"
case .favorites:
return "star"
case .profileDirectory: case .profileDirectory:
return "person.2.fill" return "person.2.fill"
case .list(_): case .list(_):

View File

@ -232,9 +232,9 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
tabBarViewController.select(tab: .explore) tabBarViewController.select(tab: .explore)
case .bookmarks, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_): case .bookmarks, .favorites, .profileDirectory, .list(_), .savedHashtag(_), .savedInstance(_):
tabBarViewController.select(tab: .explore) tabBarViewController.select(tab: .explore)
// Make sure the Explore VC doesn't show it's search bar when it appears, in case the user was previously // Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
// in compact mode and performing a search. // in compact mode and performing a search.
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
let explore = exploreNav.viewControllers.first as! ExploreViewController let explore = exploreNav.viewControllers.first as! ExploreViewController
@ -305,6 +305,8 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
switch tabNavigationStack[1] { switch tabNavigationStack[1] {
case is BookmarksViewController: case is BookmarksViewController:
exploreItem = .bookmarks exploreItem = .bookmarks
case is FavoritesViewController:
exploreItem = .favorites
case let listVC as ListTimelineViewController: case let listVC as ListTimelineViewController:
exploreItem = .list(listVC.list) exploreItem = .list(listVC.list)
case let hashtagVC as HashtagTimelineViewController: case let hashtagVC as HashtagTimelineViewController:
@ -380,6 +382,8 @@ fileprivate extension MainSidebarViewController.Item {
return SearchViewController(mastodonController: mastodonController) return SearchViewController(mastodonController: mastodonController)
case .bookmarks: case .bookmarks:
return BookmarksViewController(mastodonController: mastodonController) return BookmarksViewController(mastodonController: mastodonController)
case .favorites:
return FavoritesViewController(mastodonController: mastodonController)
case .profileDirectory: case .profileDirectory:
return ProfileDirectoryViewController(mastodonController: mastodonController) return ProfileDirectoryViewController(mastodonController: mastodonController)
case let .list(list): case let .list(list):

View File

@ -0,0 +1,50 @@
//
// CopyableLable.swift
// Tusker
//
// Created by Shadowfacts on 2/4/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
class CopyableLable: UILabel {
private var _editMenuInteraction: Any!
@available(iOS 16.0, *)
private var editMenuInteraction: UIEditMenuInteraction {
get { _editMenuInteraction as! UIEditMenuInteraction }
set { _editMenuInteraction = newValue }
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
if #available(iOS 16.0, *) {
editMenuInteraction = UIEditMenuInteraction(delegate: nil)
addInteraction(editMenuInteraction)
isUserInteractionEnabled = true
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressed)))
}
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = text
}
@available(iOS 16.0, *)
@objc private func longPressed(_ recognizer: UILongPressGestureRecognizer) {
if recognizer.state == .began {
editMenuInteraction.presentEditMenu(with: UIEditMenuConfiguration(identifier: nil, sourcePoint: CGPoint(x: bounds.midX, y: bounds.midY)))
}
}
}

View File

@ -111,7 +111,7 @@
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC"> <stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="jwU-EH-hmC">
<rect key="frame" x="144" y="235" width="103.5" height="23"/> <rect key="frame" x="144" y="235" width="103.5" height="23"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL" customClass="CopyableLable" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="2.5" width="81" height="18"/> <rect key="frame" x="0.0" y="2.5" width="81" height="18"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/> <fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
<color key="textColor" systemColor="secondaryLabelColor"/> <color key="textColor" systemColor="secondaryLabelColor"/>