Compare commits
8 Commits
ca5ac8b826
...
2b22180191
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 2b22180191 | |
Shadowfacts | 654b5d9c59 | |
Shadowfacts | 777d1f378c | |
Shadowfacts | 3b132ab4dc | |
Shadowfacts | d1083116e0 | |
Shadowfacts | 7b79cec0ed | |
Shadowfacts | 50cbbb86fc | |
Shadowfacts | 5a914ea5a3 |
|
@ -12,6 +12,7 @@ public class Mention: Codable {
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let username: String
|
public let username: String
|
||||||
public let acct: String
|
public let acct: String
|
||||||
|
/// The instance-local ID of the user being mentioned.
|
||||||
public let id: String
|
public let id: String
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
|
|
@ -154,7 +154,6 @@
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||||
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
|
D64F80E2215875CC00BEF393 /* XCBActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64F80E1215875CC00BEF393 /* XCBActionType.swift */; };
|
||||||
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */; };
|
|
||||||
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
|
D65234D325618EFA001AF9CF /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */; };
|
||||||
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */; };
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
||||||
|
@ -559,7 +558,6 @@
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
||||||
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
|
D64F80E1215875CC00BEF393 /* XCBActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActionType.swift; sourceTree = "<group>"; };
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeTableViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
D65234E02561AA68001AF9CF /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1523,7 +1521,6 @@
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||||
|
@ -2080,7 +2077,6 @@
|
||||||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
|
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
|
||||||
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */,
|
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
|
|
|
@ -99,6 +99,11 @@
|
||||||
value = "1"
|
value = "1"
|
||||||
isEnabled = "NO">
|
isEnabled = "NO">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "DEBUG_BLUR_HASH"
|
||||||
|
value = "1"
|
||||||
|
isEnabled = "NO">
|
||||||
|
</EnvironmentVariable>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
|
|
|
@ -19,6 +19,8 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
super.init(style: .grouped)
|
super.init(style: .grouped)
|
||||||
|
|
||||||
|
dragEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -33,8 +35,6 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
|
||||||
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
|
tableView.register(UINib(nibName: "TrendingHashtagTableViewCell", bundle: .main), forCellReuseIdentifier: "trendingTagCell")
|
||||||
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
|
tableView.rowHeight = 60 // 44 for content + 2 * 8 spacing
|
||||||
|
|
||||||
// todo: enable drag
|
|
||||||
|
|
||||||
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { (tableView, indexPath, item) in
|
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) { (tableView, indexPath, item) in
|
||||||
switch item {
|
switch item {
|
||||||
case let .tag(hashtag):
|
case let .tag(hashtag):
|
||||||
|
@ -75,6 +75,31 @@ class TrendingHashtagsViewController: EnhancedTableViewController {
|
||||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
case let .tag(hashtag) = item else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
|
HashtagTimelineViewController(for: hashtag, mastodonController: self.mastodonController)
|
||||||
|
} actionProvider: { (_) in
|
||||||
|
UIMenu(children: self.actionsForHashtag(hashtag, sourceView: self.tableView.cellForRow(at: indexPath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||||
|
case let .tag(hashtag) = item else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let provider = NSItemProvider(object: hashtag.url as NSURL)
|
||||||
|
if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: mastodonController.accountInfo!.id) {
|
||||||
|
provider.registerObject(activity, visibility: .all)
|
||||||
|
}
|
||||||
|
return [UIDragItem(itemProvider: provider)]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController {
|
extension TrendingHashtagsViewController {
|
||||||
|
@ -85,3 +110,11 @@ extension TrendingHashtagsViewController {
|
||||||
case tag(Hashtag)
|
case tag(Hashtag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TrendingHashtagsViewController: TuskerNavigationDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TrendingHashtagsViewController: MenuPreviewProvider {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { self }
|
||||||
|
}
|
||||||
|
|
|
@ -125,7 +125,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
// todo: does this need to be in viewDidLayoutSubviews?
|
|
||||||
// limit the image height to the safe area height, so the image doesn't overlap the top controls
|
// limit the image height to the safe area height, so the image doesn't overlap the top controls
|
||||||
// while zoomed all the way out
|
// while zoomed all the way out
|
||||||
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
|
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
|
||||||
|
@ -138,7 +137,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
|
|
||||||
centerImage()
|
centerImage()
|
||||||
|
|
||||||
// todo: does this need to be in viewDidLayoutSubviews?
|
|
||||||
let notchedDeviceTopInsets: [CGFloat] = [
|
let notchedDeviceTopInsets: [CGFloat] = [
|
||||||
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
|
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
|
||||||
48, // iPhone XR, 11
|
48, // iPhone XR, 11
|
||||||
|
|
|
@ -178,7 +178,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
private func createActivityIndicatorHeader() {
|
private func createActivityIndicatorHeader() {
|
||||||
let header = UITableViewHeaderFooterView()
|
let header = UITableViewHeaderFooterView()
|
||||||
header.translatesAutoresizingMaskIntoConstraints = false
|
header.translatesAutoresizingMaskIntoConstraints = false
|
||||||
header.contentView.backgroundColor = .secondarySystemBackground
|
header.contentView.backgroundColor = .systemGroupedBackground
|
||||||
|
|
||||||
activityIndicator = UIActivityIndicatorView(style: .large)
|
activityIndicator = UIActivityIndicatorView(style: .large)
|
||||||
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEntry> {
|
class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<ProfileStatusesViewController.Section, ProfileStatusesViewController.Item> {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
@ -43,106 +43,128 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
loadInitial()
|
if isViewLoaded {
|
||||||
|
reloadInitial()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override class func refreshCommandTitle() -> String {
|
override class func refreshCommandTitle() -> String {
|
||||||
return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title")
|
return NSLocalizedString("Refresh Statuses", comment: "refresh statuses command discoverability title")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func headerSectionsCount() -> Int {
|
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
|
||||||
return 1
|
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
||||||
|
|
||||||
|
cell.delegate = self
|
||||||
|
// todo: dataSource.sectionIdentifier is only available on iOS 15
|
||||||
|
cell.showPinned = dataSource.snapshot().indexOfSection(.pinned) == indexPath.section
|
||||||
|
cell.updateUI(statusID: item.id, state: item.state)
|
||||||
|
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadInitial() {
|
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
guard accountID != nil else {
|
guard accountID != nil else {
|
||||||
|
completion(.failure(.noClient))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !loaded {
|
|
||||||
loadPinnedStatuses()
|
|
||||||
}
|
|
||||||
|
|
||||||
super.loadInitial()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadPinnedStatuses() {
|
|
||||||
guard kind == .statuses else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
getPinnedStatuses { (response) in
|
|
||||||
guard case let .success(statuses, _) = response,
|
|
||||||
!statuses.isEmpty else {
|
|
||||||
// todo: error message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
let items = statuses.map { ($0.id, StatusState.unknown) }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
if self.sections.count < 1 {
|
|
||||||
self.sections.append(items)
|
|
||||||
self.tableView.insertSections(IndexSet(integer: 0), with: .none)
|
|
||||||
} else {
|
|
||||||
self.sections[0] = items
|
|
||||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) {
|
|
||||||
getStatuses { (response) in
|
getStatuses { (response) in
|
||||||
guard case let .success(statuses, pagination) = response,
|
switch response {
|
||||||
!statuses.isEmpty else {
|
case let .failure(error):
|
||||||
// todo: error message
|
completion(.failure(.client(error)))
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
case let .success(statuses, pagination):
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
DispatchQueue.main.async {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.appendSections([.statuses])
|
||||||
|
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
|
if self.kind == .statuses {
|
||||||
|
self.loadPinnedStatuses(snapshot: { snapshot }, completion: completion)
|
||||||
|
} else {
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) {
|
private func loadPinnedStatuses(snapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
|
guard kind == .statuses else {
|
||||||
|
completion(.success(snapshot()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getPinnedStatuses { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(statuses, _):
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
var snapshot = snapshot()
|
||||||
|
if snapshot.indexOfSection(.pinned) != nil {
|
||||||
|
snapshot.deleteSections([.pinned])
|
||||||
|
}
|
||||||
|
snapshot.insertSections([.pinned], beforeSection: .statuses)
|
||||||
|
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .pinned)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard let older = older else {
|
guard let older = older else {
|
||||||
completion([])
|
completion(.failure(.noOlder))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatuses(for: older) { (response) in
|
getStatuses(for: older) { (response) in
|
||||||
guard case let .success(statuses, pagination) = response else {
|
switch response {
|
||||||
// todo: error message
|
case let .failure(error):
|
||||||
completion([])
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(statuses, pagination):
|
||||||
|
guard !statuses.isEmpty else {
|
||||||
|
completion(.failure(.noOlder))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.older = pagination?.older
|
if let older = pagination?.older {
|
||||||
|
self.older = older
|
||||||
|
}
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
var snapshot = currentSnapshot()
|
||||||
|
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) {
|
|
||||||
|
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard let newer = newer else {
|
guard let newer = newer else {
|
||||||
completion([])
|
completion(.failure(.noNewer))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatuses(for: newer) { (response) in
|
getStatuses(for: newer) { (response) in
|
||||||
guard case let .success(statuses, pagination) = response else {
|
switch response {
|
||||||
// todo: error message
|
case let .failure(error):
|
||||||
completion([])
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(statuses, pagination):
|
||||||
|
guard !statuses.isEmpty else {
|
||||||
|
completion(.failure(.noNewer))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +173,15 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
completion(statuses.map { ($0.id, .unknown) })
|
var snapshot = currentSnapshot()
|
||||||
|
let items = statuses.map { Item(id: $0.id, state: .unknown) }
|
||||||
|
if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
|
||||||
|
snapshot.insertItems(items, beforeItem: first)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems(items, toSection: .statuses)
|
||||||
|
}
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,52 +208,19 @@ class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEnt
|
||||||
super.refresh()
|
super.refresh()
|
||||||
|
|
||||||
if kind == .statuses {
|
if kind == .statuses {
|
||||||
getPinnedStatuses { (response) in
|
loadPinnedStatuses(snapshot: dataSource.snapshot) { (result) in
|
||||||
guard case let .success(newPinnedStatues, _) = response else {
|
switch result {
|
||||||
// todo: error message
|
case .failure(_):
|
||||||
return
|
break
|
||||||
}
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatues) {
|
case let .success(snapshot):
|
||||||
// if the user refreshes before the initial pinned statuses request completes, self.sections will be empty
|
|
||||||
let oldPinnedStatuses = self.sections.isEmpty ? [] : self.sections[0]
|
|
||||||
let pinnedStatues = newPinnedStatues.map { (status) -> TimelineEntry in
|
|
||||||
let state: StatusState
|
|
||||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
|
||||||
state = oldState
|
|
||||||
} else {
|
|
||||||
state = .unknown
|
|
||||||
}
|
|
||||||
return (status.id, state)
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
UIView.performWithoutAnimation {
|
self.dataSource.apply(snapshot)
|
||||||
if self.sections.count < 1 {
|
|
||||||
self.sections.append(pinnedStatues)
|
|
||||||
self.tableView.insertSections(IndexSet(integer: 0), with: .none)
|
|
||||||
} else {
|
|
||||||
self.sections[0] = pinnedStatues
|
|
||||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDatasource
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
|
||||||
|
|
||||||
cell.delegate = self
|
|
||||||
cell.showPinned = indexPath.section == 0
|
|
||||||
|
|
||||||
let (id, state) = item(for: indexPath)
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,6 +230,17 @@ extension ProfileStatusesViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ProfileStatusesViewController {
|
||||||
|
enum Section: CaseIterable {
|
||||||
|
case pinned
|
||||||
|
case statuses
|
||||||
|
}
|
||||||
|
struct Item: Hashable {
|
||||||
|
let id: String
|
||||||
|
let state: StatusState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController: TuskerNavigationDelegate {
|
extension ProfileStatusesViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
@ -245,18 +253,12 @@ extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
||||||
|
|
||||||
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
let ids = indexPaths.map { item(for: $0).id }
|
let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id }
|
||||||
prefetchStatuses(with: ids)
|
prefetchStatuses(with: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
let ids: [String] = indexPaths.compactMap {
|
let ids = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id }
|
||||||
guard $0.section < sections.count,
|
|
||||||
$0.row < sections[$0.section].count else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return item(for: $0).id
|
|
||||||
}
|
|
||||||
cancelPrefetchingStatuses(with: ids)
|
cancelPrefetchingStatuses(with: ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,9 +211,8 @@ class ProfileViewController: UIPageViewController {
|
||||||
// Layout and update the table view, otherwise the content jumps around when first scrolling it,
|
// Layout and update the table view, otherwise the content jumps around when first scrolling it,
|
||||||
// if old was not scrolled all the way to the top
|
// if old was not scrolled all the way to the top
|
||||||
new.tableView.layoutIfNeeded()
|
new.tableView.layoutIfNeeded()
|
||||||
UIView.performWithoutAnimation {
|
let snapshot = new.dataSource.snapshot()
|
||||||
new.tableView.performBatchUpdates(nil, completion: nil)
|
new.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
}
|
|
||||||
|
|
||||||
completion?(finished)
|
completion?(finished)
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,10 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
let sectionsToRemove = contentSections[lastVisibleContentSectionIndex...]
|
let sectionsToRemove = contentSections[lastVisibleContentSectionIndex...]
|
||||||
snapshot.deleteSections(Array(sectionsToRemove))
|
snapshot.deleteSections(Array(sectionsToRemove))
|
||||||
|
|
||||||
willRemoveItems(sectionsToRemove.flatMap(snapshot.itemIdentifiers(inSection:)))
|
let itemsToRemove = sectionsToRemove.filter {
|
||||||
|
snapshot.indexOfSection($0) != nil
|
||||||
|
}.flatMap(snapshot.itemIdentifiers(inSection:))
|
||||||
|
willRemoveItems(itemsToRemove)
|
||||||
} else if lastVisibleContentSectionIndex == contentSections.count - 1 {
|
} else if lastVisibleContentSectionIndex == contentSections.count - 1 {
|
||||||
let items = snapshot.itemIdentifiers(inSection: lastVisibleRowSection)
|
let items = snapshot.itemIdentifiers(inSection: lastVisibleRowSection)
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,6 @@ extension MenuPreviewProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
let bookmarked = status.bookmarked ?? false
|
let bookmarked = status.bookmarked ?? false
|
||||||
let muted = status.muted
|
|
||||||
|
|
||||||
var actionsSection = [
|
var actionsSection = [
|
||||||
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in
|
||||||
|
@ -168,15 +167,6 @@ extension MenuPreviewProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createAction(identifier: "mute", title: muted ? "Unmute" : "Mute", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
|
|
||||||
guard let self = self else { return }
|
|
||||||
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
|
|
||||||
self.mastodonController?.run(request) { (response) in
|
|
||||||
if case let .success(status, _) = response {
|
|
||||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if includeReply {
|
if includeReply {
|
||||||
|
@ -186,7 +176,23 @@ extension MenuPreviewProvider {
|
||||||
}), at: 0)
|
}), at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
if let account = mastodonController.account {
|
||||||
|
// only allow muting conversations that either current user posted or is participating in (technically, is mentioned, since that's the best we can do)
|
||||||
|
if status.account.id == account.id || status.mentions.contains(where: { $0.id == account.id }) {
|
||||||
|
let muted = status.muted
|
||||||
|
actionsSection.append(createAction(identifier: "mute", title: muted ? "Unmute Conversation" : "Mute Conversation", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
|
||||||
|
self.mastodonController?.run(request) { (response) in
|
||||||
|
if case let .success(status, _) = response {
|
||||||
|
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// only allowing pinning user's own statuses
|
||||||
|
if account.id == status.account.id {
|
||||||
let pinned = status.pinned ?? false
|
let pinned = status.pinned ?? false
|
||||||
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
@ -199,6 +205,7 @@ extension MenuPreviewProvider {
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if status.poll != nil {
|
if status.poll != nil {
|
||||||
actionsSection.insert(createAction(identifier: "refresh", title: "Refresh Poll", systemImageName: "arrow.clockwise", handler: { [weak self] (_) in
|
actionsSection.insert(createAction(identifier: "refresh", title: "Refresh Poll", systemImageName: "arrow.clockwise", handler: { [weak self] (_) in
|
||||||
|
|
|
@ -1,254 +0,0 @@
|
||||||
//
|
|
||||||
// TimelineLikeTableViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/15/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
/// A table view controller that manages common functionality between timeline-like UIs.
|
|
||||||
/// For example, this class handles loading new items when the user scrolls to the end,
|
|
||||||
/// refreshing, and pruning offscreen rows automatically.
|
|
||||||
class TimelineLikeTableViewController<Item>: EnhancedTableViewController, RefreshableViewController {
|
|
||||||
|
|
||||||
private(set) var loaded = false
|
|
||||||
|
|
||||||
var sections: [[Item]] = []
|
|
||||||
|
|
||||||
private let pageSize = 20
|
|
||||||
|
|
||||||
private var lastLastVisibleRow: IndexPath?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(style: .plain)
|
|
||||||
|
|
||||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: Self.refreshCommandTitle()))
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func item(for indexPath: IndexPath) -> Item {
|
|
||||||
return sections[indexPath.section][indexPath.row]
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
|
||||||
tableView.estimatedRowHeight = 140
|
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
self.refreshControl = UIRefreshControl()
|
|
||||||
self.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if let prefetchSource = self as? UITableViewDataSourcePrefetching {
|
|
||||||
tableView.prefetchDataSource = prefetchSource
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
loadInitial()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
|
|
||||||
pruneOffscreenRows()
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadInitial() {
|
|
||||||
guard !loaded else { return }
|
|
||||||
// set loaded immediately so we don't trigger another request while the current one is running
|
|
||||||
loaded = true
|
|
||||||
|
|
||||||
loadInitialItems() { (items) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard items.count > 0 else {
|
|
||||||
// set loaded back to false so the next time the VC appears, we try to load again
|
|
||||||
// todo: this should probably retry automatically
|
|
||||||
self.loaded = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.sections.count < self.headerSectionsCount() {
|
|
||||||
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sections.append(items)
|
|
||||||
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func reloadInitialItems() {
|
|
||||||
loaded = false
|
|
||||||
sections = []
|
|
||||||
loadInitial()
|
|
||||||
}
|
|
||||||
|
|
||||||
func cellHeightChanged() {
|
|
||||||
// causes the table view to recalculate the cell heights
|
|
||||||
tableView.beginUpdates()
|
|
||||||
tableView.endUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
class func refreshCommandTitle() -> String {
|
|
||||||
return "Refresh"
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: these three should use Result<[Item], Client.Error> so we can differentiate between failed requests and there actually being no results
|
|
||||||
|
|
||||||
func loadInitialItems(completion: @escaping ([Item]) -> Void) {
|
|
||||||
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadOlder(completion: @escaping ([Item]) -> Void) {
|
|
||||||
fatalError("loadOlder(completion:) must be implemented by subclasses")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadNewer(completion: @escaping ([Item]) -> Void) {
|
|
||||||
fatalError("loadNewer(completion:) must be implemented by subclasses")
|
|
||||||
}
|
|
||||||
|
|
||||||
func willRemoveRows(at indexPaths: [IndexPath]) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func headerSectionsCount() -> Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private func pruneOffscreenRows() {
|
|
||||||
guard let lastVisibleRow = lastLastVisibleRow,
|
|
||||||
// never remove the last section
|
|
||||||
sections.count - headerSectionsCount() > 1 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let lastSectionIndex = sections.count - 1
|
|
||||||
|
|
||||||
if lastVisibleRow.section < lastSectionIndex {
|
|
||||||
// if there is a section below the last visible one
|
|
||||||
|
|
||||||
let sectionsToRemove = (lastVisibleRow.section + 1)...lastSectionIndex
|
|
||||||
|
|
||||||
let indexPathsToRemove = sectionsToRemove.flatMap { (section) in
|
|
||||||
sections[section].indices.map { (row) in
|
|
||||||
IndexPath(row: row, section: section)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
willRemoveRows(at: indexPathsToRemove)
|
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
tableView.deleteSections(IndexSet(sectionsToRemove), with: .none)
|
|
||||||
}
|
|
||||||
|
|
||||||
sections.removeSubrange(sectionsToRemove)
|
|
||||||
} else if lastVisibleRow.section == lastSectionIndex {
|
|
||||||
let lastSection = sections.last!
|
|
||||||
let lastRowIndex = lastSection.count - 1
|
|
||||||
|
|
||||||
if lastVisibleRow.row < lastRowIndex - pageSize {
|
|
||||||
// if there are more than pageSize rows in the current section below the last visible one
|
|
||||||
|
|
||||||
let rowIndicesInLastSectionToRemove = (lastVisibleRow.row + pageSize)..<lastSection.count
|
|
||||||
|
|
||||||
let indexPathsToRemove = rowIndicesInLastSectionToRemove.map {
|
|
||||||
IndexPath(row: $0, section: lastSectionIndex)
|
|
||||||
}
|
|
||||||
willRemoveRows(at: indexPathsToRemove)
|
|
||||||
|
|
||||||
sections[lastSectionIndex].removeSubrange(rowIndicesInLastSectionToRemove)
|
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
tableView.deleteRows(at: indexPathsToRemove, with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
|
||||||
return sections.count
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
return sections[section].count
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
fatalError("tableView(_:cellForRowAt:) must be implemented by subclasses")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
|
||||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
||||||
// this assumes that indexPathsForVisibleRows is always in order
|
|
||||||
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
|
||||||
|
|
||||||
if indexPath.section == sections.count - 1,
|
|
||||||
indexPath.row == sections[indexPath.section].count - 1 {
|
|
||||||
loadOlder() { (newItems) in
|
|
||||||
guard newItems.count > 0 else { return }
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let newRows = self.sections.last!.count..<(self.sections.last!.count + newItems.count)
|
|
||||||
let newIndexPaths = newRows.map { IndexPath(row: $0, section: self.sections.count - 1) }
|
|
||||||
|
|
||||||
self.sections[self.sections.count - 1].append(contentsOf: newItems)
|
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - RefreshableViewController
|
|
||||||
func refresh() {
|
|
||||||
loadNewer() { (newItems) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.refreshControl?.endRefreshing()
|
|
||||||
|
|
||||||
guard newItems.count > 0 else { return }
|
|
||||||
|
|
||||||
let firstNonHeaderSection = self.headerSectionsCount()
|
|
||||||
|
|
||||||
self.sections[firstNonHeaderSection].insert(contentsOf: newItems, at: 0)
|
|
||||||
|
|
||||||
let newIndexPaths = (0..<newItems.count).map { IndexPath(row: $0, section: firstNonHeaderSection) }
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertRows(at: newIndexPaths, with: .none)
|
|
||||||
}
|
|
||||||
|
|
||||||
// maintain the current position in the list (don't scroll to top)
|
|
||||||
self.tableView.scrollToRow(at: IndexPath(row: newItems.count, section: firstNonHeaderSection), at: .top, animated: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TimelineLikeTableViewController: BackgroundableViewController {
|
|
||||||
func sceneDidEnterBackground() {
|
|
||||||
pruneOffscreenRows()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,6 +17,8 @@ protocol AttachmentViewDelegate: AnyObject {
|
||||||
|
|
||||||
class AttachmentView: GIFImageView {
|
class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
|
static let queue = DispatchQueue(label: "Attachment Thumbnail", qos: .userInitiated, attributes: .concurrent)
|
||||||
|
|
||||||
weak var delegate: AttachmentViewDelegate?
|
weak var delegate: AttachmentViewDelegate?
|
||||||
|
|
||||||
var playImageView: UIImageView?
|
var playImageView: UIImageView?
|
||||||
|
@ -108,7 +110,7 @@ class AttachmentView: GIFImageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let hash = attachment.blurHash {
|
if let hash = attachment.blurHash {
|
||||||
DispatchQueue.global(qos: .default).async { [weak self] in
|
AttachmentView.queue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let size: CGSize
|
let size: CGSize
|
||||||
if let meta = self.attachment.meta,
|
if let meta = self.attachment.meta,
|
||||||
|
@ -191,8 +193,7 @@ class AttachmentView: GIFImageView {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let attachmentURL = self.attachment.url
|
let attachmentURL = self.attachment.url
|
||||||
// todo: use a single dispatch queue
|
AttachmentView.queue.async {
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
let asset = AVURLAsset(url: attachmentURL)
|
let asset = AVURLAsset(url: attachmentURL)
|
||||||
let generator = AVAssetImageGenerator(asset: asset)
|
let generator = AVAssetImageGenerator(asset: asset)
|
||||||
generator.appliesPreferredTrackTransform = true
|
generator.appliesPreferredTrackTransform = true
|
||||||
|
@ -237,7 +238,7 @@ class AttachmentView: GIFImageView {
|
||||||
func loadGifv() {
|
func loadGifv() {
|
||||||
let attachmentURL = self.attachment.url
|
let attachmentURL = self.attachment.url
|
||||||
let asset = AVURLAsset(url: attachmentURL)
|
let asset = AVURLAsset(url: attachmentURL)
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
AttachmentView.queue.async {
|
||||||
let generator = AVAssetImageGenerator(asset: asset)
|
let generator = AVAssetImageGenerator(asset: asset)
|
||||||
generator.appliesPreferredTrackTransform = true
|
generator.appliesPreferredTrackTransform = true
|
||||||
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
||||||
|
|
|
@ -15,6 +15,9 @@ class HashtagHistoryView: UIView {
|
||||||
|
|
||||||
private let curveRadius: CGFloat = 10
|
private let curveRadius: CGFloat = 10
|
||||||
|
|
||||||
|
/// The base background color used for the graph fill.
|
||||||
|
var effectiveBackgroundColor = UIColor.systemBackground
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
@ -121,7 +124,7 @@ class HashtagHistoryView: UIView {
|
||||||
var tintGreen: CGFloat = 0
|
var tintGreen: CGFloat = 0
|
||||||
var tintBlue: CGFloat = 0
|
var tintBlue: CGFloat = 0
|
||||||
traitCollection.performAsCurrent {
|
traitCollection.performAsCurrent {
|
||||||
backgroundColor!.getRed(&backgroundRed, green: &backgroundGreen, blue: &backgroundBlue, alpha: nil)
|
effectiveBackgroundColor.getRed(&backgroundRed, green: &backgroundGreen, blue: &backgroundBlue, alpha: nil)
|
||||||
tintColor.getRed(&tintRed, green: &tintGreen, blue: &tintBlue, alpha: nil)
|
tintColor.getRed(&tintRed, green: &tintGreen, blue: &tintBlue, alpha: nil)
|
||||||
}
|
}
|
||||||
let blendedRed = (backgroundRed + tintRed) / 2
|
let blendedRed = (backgroundRed + tintRed) / 2
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18121" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
</stackView>
|
</stackView>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xrw-2v-ybZ" customClass="HashtagHistoryView" customModule="Tusker" customModuleProvider="target">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Xrw-2v-ybZ" customClass="HashtagHistoryView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="188" y="11" width="100" height="44"/>
|
<rect key="frame" x="188" y="11" width="100" height="44"/>
|
||||||
<color key="backgroundColor" systemColor="secondarySystemGroupedBackgroundColor"/>
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="44" id="W4C-uw-zWg"/>
|
<constraint firstAttribute="height" constant="44" id="W4C-uw-zWg"/>
|
||||||
<constraint firstAttribute="width" constant="100" id="XHb-vd-qNk"/>
|
<constraint firstAttribute="width" constant="100" id="XHb-vd-qNk"/>
|
||||||
|
@ -64,9 +64,4 @@
|
||||||
<point key="canvasLocation" x="132" y="132"/>
|
<point key="canvasLocation" x="132" y="132"/>
|
||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
|
||||||
<systemColor name="secondarySystemGroupedBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
</document>
|
||||||
|
|
|
@ -163,8 +163,7 @@ class StatusCardView: UIView {
|
||||||
|
|
||||||
let imageViewSize = self.imageView.bounds.size
|
let imageViewSize = self.imageView.bounds.size
|
||||||
|
|
||||||
// todo: merge this code with AttachmentView, use a single DispatchQueue
|
AttachmentView.queue.async { [weak self] in
|
||||||
DispatchQueue.global(qos: .default).async { [weak self] in
|
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
let size: CGSize
|
let size: CGSize
|
||||||
|
|
Loading…
Reference in New Issue