Re-add public timeline descriptions

This commit is contained in:
Shadowfacts 2022-10-01 15:32:06 -04:00
parent 253fb8d27d
commit a38c89a17f
5 changed files with 140 additions and 22 deletions

View File

@ -215,6 +215,7 @@
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */; };
D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; };
D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.framework */; };
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */; };
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; };
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */; };
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; };
@ -560,6 +561,7 @@
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = "<group>"; };
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineDescriptionCollectionViewCell.swift; sourceTree = "<group>"; };
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
@ -897,6 +899,7 @@
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */,
D65234D225618EFA001AF9CF /* TimelineTableViewController.swift */,
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */,
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */,
);
path = Timeline;
sourceTree = "<group>";
@ -1889,6 +1892,7 @@
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */,
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
D6ADB6E828E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
D622757824EE133700B82A16 /* ComposeAssetPicker.swift in Sources */,
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,

View File

@ -0,0 +1,62 @@
//
// PublicTimelineDescriptionCollectionViewCell.swift
// Tusker
//
// Created by Shadowfacts on 10/1/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import UIKit
class PublicTimelineDescriptionCollectionViewCell: UICollectionViewCell {
weak var mastodonController: MastodonController!
var local = false {
didSet {
updateLabel()
}
}
var didDismiss: (() -> Void)?
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .tintColor
label.font = .preferredFont(forTextStyle: .body)
label.numberOfLines = 0
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1),
contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1),
label.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1),
contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: label.bottomAnchor, multiplier: 1),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateLabel() {
let str = NSMutableAttributedString()
let instanceStr = NSAttributedString(string: mastodonController.instanceURL.host!, attributes: [
.font: UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0)
])
if local {
str.append(NSAttributedString(string: "The local timeline shows public posts from only "))
str.append(instanceStr)
str.append(NSAttributedString(string: "."))
} else {
str.append(NSAttributedString(string: "The federated timeline shows public posts from all users that "))
str.append(instanceStr)
str.append(NSAttributedString(string: " knows about."))
}
label.attributedText = str
}
}

View File

@ -22,6 +22,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
let confirmLoadMore = PassthroughSubject<Void, Never>()
private var newer: RequestRange?
private var older: RequestRange?
// stored separately because i don't want to query the snapshot every time the user scrolls
private var isShowingTimelineDescription = false
var collectionView: UICollectionView {
view as! UICollectionView
@ -63,6 +65,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
// collectionView.dragDelegate = self
registerTimelineLikeCells()
collectionView.register(PublicTimelineDescriptionCollectionViewCell.self, forCellWithReuseIdentifier: "publicTimelineDescription")
dataSource = createDataSource()
applyInitialSnapshot()
@ -94,6 +97,16 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
config.text = try! doc.text()
cell.contentConfiguration = config
}
let timelineDescriptionCell = UICollectionView.CellRegistration<PublicTimelineDescriptionCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
guard case .public(let local) = timeline else {
fatalError()
}
cell.mastodonController = self.mastodonController
cell.local = local
cell.didDismiss = { [unowned self] in
self.removeTimelineDescriptionCell()
}
}
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .status(_, _):
@ -102,13 +115,22 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
return loadingIndicatorCell(for: indexPath)
case .confirmLoadMore:
return confirmLoadMoreCell(for: indexPath)
case .publicTimelineDescription:
self.isShowingTimelineDescription = true
return collectionView.dequeueConfiguredReusableCell(using: timelineDescriptionCell, for: indexPath, item: itemIdentifier)
}
}
}
private func applyInitialSnapshot() {
// TODO: this might not be necessary
// TODO: yes it is, for public timeline descriptions
if case .public(let local) = timeline,
(local && !Preferences.shared.hasShownLocalTimelineDescription) ||
(!local && Preferences.shared.hasShownFederatedTimelineDescription) {
var snapshot = dataSource.snapshot()
snapshot.appendSections([.header])
snapshot.appendItems([.publicTimelineDescription], toSection: .header)
dataSource.apply(snapshot, animatingDifferences: false)
}
}
override func viewWillAppear(_ animated: Bool) {
@ -119,6 +141,13 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
}
private func removeTimelineDescriptionCell() {
var snapshot = dataSource.snapshot()
snapshot.deleteSections([.header])
dataSource.apply(snapshot, animatingDifferences: true)
isShowingTimelineDescription = false
}
}
extension TimelineViewController {
@ -135,8 +164,7 @@ extension TimelineViewController {
case status(id: String, state: StatusState)
case loadingIndicator
case confirmLoadMore
// // TODO: remove local param from this
// case publicTimelineDescription(local: Bool)
case publicTimelineDescription
static func fromTimelineItem(_ id: String) -> Self {
return .status(id: id, state: .unknown)
@ -150,8 +178,8 @@ extension TimelineViewController {
return true
case (.confirmLoadMore, .confirmLoadMore):
return true
// case let (.publicTimelineDescription(local: a), .publicTimelineDescription(local: b)):
// return a == b
case (.publicTimelineDescription, .publicTimelineDescription):
return true
default:
return false
}
@ -166,15 +194,14 @@ extension TimelineViewController {
hasher.combine(1)
case .confirmLoadMore:
hasher.combine(2)
// case .publicTimelineDescription(local: let local):
// hasher.combine(3)
// hasher.combine(local)
case .publicTimelineDescription:
hasher.combine(3)
}
}
var hideSeparators: Bool {
switch self {
case .loadingIndicator:
case .loadingIndicator, .publicTimelineDescription:
return true
default:
return false
@ -192,8 +219,6 @@ extension TimelineViewController {
throw Error.noClient
}
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
let request = Client.getStatuses(timeline: timeline)
let (statuses, _) = try await mastodonController.run(request)
@ -235,8 +260,6 @@ extension TimelineViewController {
throw Error.noOlder
}
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
let request = Client.getStatuses(timeline: timeline, range: older)
let (statuses, _) = try await mastodonController.run(request)
@ -273,4 +296,24 @@ extension TimelineViewController: UICollectionViewDelegate {
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else {
return
}
switch item {
case .publicTimelineDescription:
removeTimelineDescriptionCell()
default:
// TODO: cell selection
break
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if isShowingTimelineDescription {
removeTimelineDescriptionCell()
}
}
}

View File

@ -22,10 +22,10 @@ class TimelinesPageViewController: SegmentedPageViewController {
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
home.title = homeTitle
let federated = TimelineTableViewController(for: .public(local: false), mastodonController: mastodonController)
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
federated.title = federatedTitle
let local = TimelineTableViewController(for: .public(local: true), mastodonController: mastodonController)
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
local.title = localTitle
super.init(titles: [

View File

@ -52,7 +52,7 @@ extension TimelineLikeCollectionViewController {
snapshot.appendSections([.footer])
}
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
await dataSource.apply(snapshot, animatingDifferences: false)
await apply(snapshot, animatingDifferences: false)
}
for await _ in confirmLoadMore.values {
return true
@ -94,14 +94,14 @@ extension TimelineLikeCollectionViewController {
} else {
snapshot.appendItems([.loadingIndicator], toSection: .footer)
}
await dataSource.apply(snapshot, animatingDifferences: false)
await apply(snapshot, animatingDifferences: false)
}
func handleRemoveLoadingIndicator() async {
let oldContentOffset = collectionView.contentOffset
var snapshot = dataSource.snapshot()
snapshot.deleteSections([.footer])
await dataSource.apply(snapshot, animatingDifferences: false)
await apply(snapshot, animatingDifferences: false)
// prevent the collection view from scrolling as we remove the loading indicator and add the timeline items
collectionView.contentOffset = oldContentOffset
}
@ -123,7 +123,7 @@ extension TimelineLikeCollectionViewController {
}
snapshot.appendSections([.entries])
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
await dataSource.apply(snapshot, animatingDifferences: false)
await apply(snapshot, animatingDifferences: false)
}
func handleLoadNewerError(_ error: Swift.Error) async {
@ -156,7 +156,7 @@ extension TimelineLikeCollectionViewController {
} else {
snapshot.appendItems(items, toSection: .entries)
}
await dataSource.apply(snapshot, animatingDifferences: false)
await apply(snapshot, animatingDifferences: false)
if let first,
let indexPath = dataSource.indexPath(for: first) {
@ -183,11 +183,20 @@ extension TimelineLikeCollectionViewController {
snapshot.deleteItems([.confirmLoadMore])
}
snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
await dataSource.apply(snapshot, animatingDifferences: false)
await apply(snapshot, animatingDifferences: false)
}
}
extension TimelineLikeCollectionViewController {
// apply(_:animatingDifferences:) is marked as nonisolated, so just awaiting it doesn't dispatch to the main thread, unlike other async @MainActor methods
// but we always want to update the data source on the main thread for consistency, so this method does that
func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool) async {
let task = Task { @MainActor in
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
await task.value
}
func registerTimelineLikeCells() {
collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")