Compare commits
No commits in common. "2b22180191a2217b5a53bae9bde42f5f4468a3e0" and "ca5ac8b826701be283bcb8cddbfcaf2808cffd66" have entirely different histories.
2b22180191
...
ca5ac8b826
|
@ -12,7 +12,6 @@ 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,6 +154,7 @@
|
||||||
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 */; };
|
||||||
|
@ -558,6 +559,7 @@
|
||||||
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>"; };
|
||||||
|
@ -1521,6 +1523,7 @@
|
||||||
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 */,
|
||||||
|
@ -2077,6 +2080,7 @@
|
||||||
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,11 +99,6 @@
|
||||||
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,8 +19,6 @@ 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) {
|
||||||
|
@ -35,6 +33,8 @@ 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,31 +75,6 @@ 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 {
|
||||||
|
@ -110,11 +85,3 @@ extension TrendingHashtagsViewController {
|
||||||
case tag(Hashtag)
|
case tag(Hashtag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: TuskerNavigationDelegate {
|
|
||||||
var apiController: MastodonController { mastodonController }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TrendingHashtagsViewController: MenuPreviewProvider {
|
|
||||||
var navigationDelegate: TuskerNavigationDelegate? { self }
|
|
||||||
}
|
|
||||||
|
|
|
@ -125,6 +125,7 @@ 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
|
||||||
|
@ -137,6 +138,7 @@ 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 = .systemGroupedBackground
|
header.contentView.backgroundColor = .secondarySystemBackground
|
||||||
|
|
||||||
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: DiffableTimelineLikeTableViewController<ProfileStatusesViewController.Section, ProfileStatusesViewController.Item> {
|
class ProfileStatusesViewController: TimelineLikeTableViewController<TimelineEntry> {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
@ -43,145 +43,115 @@ class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<Pro
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
if isViewLoaded {
|
loadInitial()
|
||||||
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 cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ item: Item) -> UITableViewCell? {
|
override func headerSectionsCount() -> Int {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
return 1
|
||||||
|
|
||||||
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 loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
override func loadInitial() {
|
||||||
guard accountID != nil else {
|
guard accountID != nil else {
|
||||||
completion(.failure(.noClient))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatuses { (response) in
|
if !loaded {
|
||||||
switch response {
|
loadPinnedStatuses()
|
||||||
case let .failure(error):
|
|
||||||
completion(.failure(.client(error)))
|
|
||||||
|
|
||||||
case let .success(statuses, pagination):
|
|
||||||
self.older = pagination?.older
|
|
||||||
self.newer = pagination?.newer
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super.loadInitial()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadPinnedStatuses(snapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
private func loadPinnedStatuses() {
|
||||||
guard kind == .statuses else {
|
guard kind == .statuses else {
|
||||||
completion(.success(snapshot()))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getPinnedStatuses { (response) in
|
getPinnedStatuses { (response) in
|
||||||
switch response {
|
guard case let .success(statuses, _) = response,
|
||||||
case let .failure(error):
|
!statuses.isEmpty else {
|
||||||
completion(.failure(.client(error)))
|
// todo: error message
|
||||||
|
return
|
||||||
case let .success(statuses, _):
|
}
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
DispatchQueue.main.async {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
var snapshot = snapshot()
|
let items = statuses.map { ($0.id, StatusState.unknown) }
|
||||||
if snapshot.indexOfSection(.pinned) != nil {
|
DispatchQueue.main.async {
|
||||||
snapshot.deleteSections([.pinned])
|
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)
|
||||||
}
|
}
|
||||||
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) {
|
override func loadInitialItems(completion: @escaping ([TimelineEntry]) -> Void) {
|
||||||
|
getStatuses { (response) in
|
||||||
|
guard case let .success(statuses, pagination) = response,
|
||||||
|
!statuses.isEmpty else {
|
||||||
|
// todo: error message
|
||||||
|
completion([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.older = pagination?.older
|
||||||
|
self.newer = pagination?.newer
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
completion(statuses.map { ($0.id, .unknown) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func loadOlder(completion: @escaping ([TimelineEntry]) -> Void) {
|
||||||
guard let older = older else {
|
guard let older = older else {
|
||||||
completion(.failure(.noOlder))
|
completion([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatuses(for: older) { (response) in
|
getStatuses(for: older) { (response) in
|
||||||
switch response {
|
guard case let .success(statuses, pagination) = response else {
|
||||||
case let .failure(error):
|
// todo: error message
|
||||||
completion(.failure(.client(error)))
|
completion([])
|
||||||
|
return
|
||||||
case let .success(statuses, pagination):
|
}
|
||||||
guard !statuses.isEmpty else {
|
|
||||||
completion(.failure(.noOlder))
|
self.older = pagination?.older
|
||||||
return
|
|
||||||
}
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
completion(statuses.map { ($0.id, .unknown) })
|
||||||
if let older = pagination?.older {
|
|
||||||
self.older = older
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
var snapshot = currentSnapshot()
|
|
||||||
snapshot.appendItems(statuses.map { Item(id: $0.id, state: .unknown) }, toSection: .statuses)
|
|
||||||
completion(.success(snapshot))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
override func loadNewer(completion: @escaping ([TimelineEntry]) -> Void) {
|
||||||
guard let newer = newer else {
|
guard let newer = newer else {
|
||||||
completion(.failure(.noNewer))
|
completion([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatuses(for: newer) { (response) in
|
getStatuses(for: newer) { (response) in
|
||||||
switch response {
|
guard case let .success(statuses, pagination) = response else {
|
||||||
case let .failure(error):
|
// todo: error message
|
||||||
completion(.failure(.client(error)))
|
completion([])
|
||||||
|
return
|
||||||
case let .success(statuses, pagination):
|
}
|
||||||
guard !statuses.isEmpty else {
|
|
||||||
completion(.failure(.noNewer))
|
if let newer = pagination?.newer {
|
||||||
return
|
self.newer = newer
|
||||||
}
|
}
|
||||||
|
|
||||||
if let newer = pagination?.newer {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
self.newer = newer
|
completion(statuses.map { ($0.id, .unknown) })
|
||||||
}
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,20 +178,53 @@ class ProfileStatusesViewController: DiffableTimelineLikeTableViewController<Pro
|
||||||
super.refresh()
|
super.refresh()
|
||||||
|
|
||||||
if kind == .statuses {
|
if kind == .statuses {
|
||||||
loadPinnedStatuses(snapshot: dataSource.snapshot) { (result) in
|
getPinnedStatuses { (response) in
|
||||||
switch result {
|
guard case let .success(newPinnedStatues, _) = response else {
|
||||||
case .failure(_):
|
// todo: error message
|
||||||
break
|
return
|
||||||
|
}
|
||||||
case let .success(snapshot):
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatues) {
|
||||||
|
// 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 {
|
||||||
self.dataSource.apply(snapshot)
|
UIView.performWithoutAnimation {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController {
|
extension ProfileStatusesViewController {
|
||||||
|
@ -230,17 +233,6 @@ 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 }
|
||||||
}
|
}
|
||||||
|
@ -253,12 +245,18 @@ 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.compactMap { dataSource.itemIdentifier(for: $0)?.id }
|
let ids = indexPaths.map { item(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 = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.id }
|
let ids: [String] = indexPaths.compactMap {
|
||||||
|
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,8 +211,9 @@ 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()
|
||||||
let snapshot = new.dataSource.snapshot()
|
UIView.performWithoutAnimation {
|
||||||
new.dataSource.apply(snapshot, animatingDifferences: false)
|
new.tableView.performBatchUpdates(nil, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
completion?(finished)
|
completion?(finished)
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,10 +88,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
let sectionsToRemove = contentSections[lastVisibleContentSectionIndex...]
|
let sectionsToRemove = contentSections[lastVisibleContentSectionIndex...]
|
||||||
snapshot.deleteSections(Array(sectionsToRemove))
|
snapshot.deleteSections(Array(sectionsToRemove))
|
||||||
|
|
||||||
let itemsToRemove = sectionsToRemove.filter {
|
willRemoveItems(sectionsToRemove.flatMap(snapshot.itemIdentifiers(inSection:)))
|
||||||
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,6 +156,7 @@ 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
|
||||||
|
@ -167,6 +168,15 @@ 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 {
|
||||||
|
@ -176,35 +186,18 @@ extension MenuPreviewProvider {
|
||||||
}), at: 0)
|
}), at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let account = mastodonController.account {
|
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
||||||
// only allow muting conversations that either current user posted or is participating in (technically, is mentioned, since that's the best we can do)
|
let pinned = status.pinned ?? false
|
||||||
if status.account.id == account.id || status.mentions.contains(where: { $0.id == account.id }) {
|
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||||
let muted = status.muted
|
guard let self = self else { return }
|
||||||
actionsSection.append(createAction(identifier: "mute", title: muted ? "Unmute Conversation" : "Mute Conversation", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in
|
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
||||||
|
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id)
|
if case let .success(status, _) = response {
|
||||||
self.mastodonController?.run(request) { (response) in
|
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||||
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
|
|
||||||
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 }
|
|
||||||
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
|
||||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
|
||||||
guard let self = self else { return }
|
|
||||||
if case let .success(status, _) = response {
|
|
||||||
self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.poll != nil {
|
if status.poll != nil {
|
||||||
|
|
|
@ -0,0 +1,254 @@
|
||||||
|
//
|
||||||
|
// 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,8 +17,6 @@ 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?
|
||||||
|
@ -110,7 +108,7 @@ class AttachmentView: GIFImageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let hash = attachment.blurHash {
|
if let hash = attachment.blurHash {
|
||||||
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
|
||||||
if let meta = self.attachment.meta,
|
if let meta = self.attachment.meta,
|
||||||
|
@ -193,7 +191,8 @@ class AttachmentView: GIFImageView {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let attachmentURL = self.attachment.url
|
let attachmentURL = self.attachment.url
|
||||||
AttachmentView.queue.async {
|
// todo: use a single dispatch queue
|
||||||
|
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
|
||||||
|
@ -238,7 +237,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)
|
||||||
AttachmentView.queue.async {
|
DispatchQueue.global(qos: .userInitiated).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,9 +15,6 @@ 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()
|
||||||
|
|
||||||
|
@ -124,7 +121,7 @@ class HashtagHistoryView: UIView {
|
||||||
var tintGreen: CGFloat = 0
|
var tintGreen: CGFloat = 0
|
||||||
var tintBlue: CGFloat = 0
|
var tintBlue: CGFloat = 0
|
||||||
traitCollection.performAsCurrent {
|
traitCollection.performAsCurrent {
|
||||||
effectiveBackgroundColor.getRed(&backgroundRed, green: &backgroundGreen, blue: &backgroundBlue, alpha: nil)
|
backgroundColor!.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="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<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">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18091"/>
|
||||||
<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" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" systemColor="secondarySystemGroupedBackgroundColor"/>
|
||||||
<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,4 +64,9 @@
|
||||||
<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,7 +163,8 @@ class StatusCardView: UIView {
|
||||||
|
|
||||||
let imageViewSize = self.imageView.bounds.size
|
let imageViewSize = self.imageView.bounds.size
|
||||||
|
|
||||||
AttachmentView.queue.async { [weak self] in
|
// todo: merge this code with AttachmentView, use a single DispatchQueue
|
||||||
|
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