feat: add reply to header for toot
This commit is contained in:
parent
807dfd9ea7
commit
75d39aabf0
|
@ -145,16 +145,19 @@ public extension Toot {
|
|||
|
||||
return toot
|
||||
}
|
||||
|
||||
func update(reblogsCount: NSNumber) {
|
||||
if self.reblogsCount.intValue != reblogsCount.intValue {
|
||||
self.reblogsCount = reblogsCount
|
||||
}
|
||||
}
|
||||
|
||||
func update(favouritesCount: NSNumber) {
|
||||
if self.favouritesCount.intValue != favouritesCount.intValue {
|
||||
self.favouritesCount = favouritesCount
|
||||
}
|
||||
}
|
||||
|
||||
func update(repliesCount: NSNumber?) {
|
||||
guard let count = repliesCount else {
|
||||
return
|
||||
|
@ -163,6 +166,13 @@ public extension Toot {
|
|||
self.repliesCount = repliesCount
|
||||
}
|
||||
}
|
||||
|
||||
func update(replyTo: Toot?) {
|
||||
if self.replyTo != replyTo {
|
||||
self.replyTo = replyTo
|
||||
}
|
||||
}
|
||||
|
||||
func update(liked: Bool, mastodonUser: MastodonUser) {
|
||||
if liked {
|
||||
if !(self.favouritedBy ?? Set()).contains(mastodonUser) {
|
||||
|
@ -174,6 +184,7 @@ public extension Toot {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(reblogged: Bool, mastodonUser: MastodonUser) {
|
||||
if reblogged {
|
||||
if !(self.rebloggedBy ?? Set()).contains(mastodonUser) {
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
},
|
||||
"status": {
|
||||
"user_boosted": "%s boosted",
|
||||
"user_replied_to": "Replied to %s",
|
||||
"show_post": "Show Post",
|
||||
"status_content_warning": "content warning",
|
||||
"media_content_warning": "Tap to reveal that may be sensitive",
|
||||
|
|
|
@ -151,6 +151,9 @@
|
|||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; };
|
||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; };
|
||||
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; };
|
||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */; };
|
||||
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */; };
|
||||
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */; };
|
||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
|
||||
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
|
||||
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
||||
|
@ -399,6 +402,9 @@
|
|||
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
|
||||
DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = "<group>"; };
|
||||
DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = "<group>"; };
|
||||
DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDataSourcePrefetching.swift"; sourceTree = "<group>"; };
|
||||
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPrefetchingService.swift; sourceTree = "<group>"; };
|
||||
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = "<group>"; };
|
||||
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
|
||||
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
|
||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -606,6 +612,7 @@
|
|||
2DF75B9A25D0E27500694EC8 /* StatusProviderFacade.swift */,
|
||||
2DF75BA025D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift */,
|
||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */,
|
||||
DB71FD4525F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift */,
|
||||
);
|
||||
path = StatusProvider;
|
||||
sourceTree = "<group>";
|
||||
|
@ -655,6 +662,7 @@
|
|||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
||||
2D206B8B25F6015000143C56 /* AudioPlayer.swift */,
|
||||
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
||||
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -933,6 +941,7 @@
|
|||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */,
|
||||
);
|
||||
path = APIService;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1566,6 +1575,7 @@
|
|||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
|
||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
|
||||
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
|
||||
|
@ -1577,6 +1587,7 @@
|
|||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
|
||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||
|
@ -1589,6 +1600,7 @@
|
|||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
|
||||
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
||||
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||
|
|
|
@ -79,12 +79,17 @@ extension StatusSection {
|
|||
statusItemAttribute: Item.StatusAttribute
|
||||
) {
|
||||
// set header
|
||||
cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil
|
||||
cell.statusView.headerInfoLabel.text = {
|
||||
let author = toot.author
|
||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userBoosted(name)
|
||||
}()
|
||||
StatusSection.configureHeader(cell: cell, toot: toot)
|
||||
ManagedObjectObserver.observe(object: toot)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { change in
|
||||
guard case .update(let object) = change.changeType,
|
||||
let newToot = object as? Toot else { return }
|
||||
StatusSection.configureHeader(cell: cell, toot: newToot)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
// set name username avatar
|
||||
cell.statusView.nameLabel.text = {
|
||||
|
@ -225,7 +230,6 @@ extension StatusSection {
|
|||
guard case .update(let object) = change.changeType,
|
||||
let newToot = object as? Toot else { return }
|
||||
let targetToot = newToot.reblog ?? newToot
|
||||
|
||||
let isLike = targetToot.favouritedBy.flatMap { $0.contains(where: { $0.id == requestUserID }) } ?? false
|
||||
let favoriteCount = targetToot.favouritesCount.intValue
|
||||
let favoriteCountTitle = StatusSection.formattedNumberTitleForActionButton(favoriteCount)
|
||||
|
@ -236,6 +240,31 @@ extension StatusSection {
|
|||
.store(in: &cell.disposeBag)
|
||||
}
|
||||
|
||||
static func configureHeader(
|
||||
cell: StatusTableViewCell,
|
||||
toot: Toot
|
||||
) {
|
||||
if toot.reblog != nil {
|
||||
cell.statusView.headerContainerStackView.isHidden = false
|
||||
cell.statusView.headerInfoLabel.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage)
|
||||
cell.statusView.headerInfoLabel.text = {
|
||||
let author = toot.author
|
||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userBoosted(name)
|
||||
}()
|
||||
} else if let replyTo = toot.replyTo {
|
||||
cell.statusView.headerContainerStackView.isHidden = false
|
||||
cell.statusView.headerInfoLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
|
||||
cell.statusView.headerInfoLabel.text = {
|
||||
let author = replyTo.author
|
||||
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||
return L10n.Common.Controls.Status.userRepliedTo(name)
|
||||
}()
|
||||
} else {
|
||||
cell.statusView.headerContainerStackView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
static func configure(
|
||||
cell: StatusTableViewCell,
|
||||
poll: Poll?,
|
||||
|
|
|
@ -80,6 +80,10 @@ internal enum L10n {
|
|||
internal static func userBoosted(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1))
|
||||
}
|
||||
/// Replied to %@
|
||||
internal static func userRepliedTo(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Status.UserRepliedTo", String(describing: p1))
|
||||
}
|
||||
internal enum Poll {
|
||||
/// Closed
|
||||
internal static let closed = L10n.tr("Localizable", "Common.Controls.Status.Poll.Closed")
|
||||
|
|
|
@ -119,7 +119,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
|
||||
guard let diffableDataSource = cell.statusView.pollTableViewDataSource else { return }
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
||||
guard case let .opion(objectID, attribute) = item else { return }
|
||||
guard case let .opion(objectID, _) = item else { return }
|
||||
guard let option = managedObjectContext.object(with: objectID) as? PollOption else { return }
|
||||
|
||||
let poll = option.poll
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// StatusProvider+UITableViewDataSourcePrefetching.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
func handleTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
// prefetch reply toot
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
|
||||
var statusObjectIDs: [NSManagedObjectID] = []
|
||||
for item in items(indexPaths: indexPaths) {
|
||||
switch item {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
let homeTimelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||
statusObjectIDs.append(homeTimelineIndex.toot.objectID)
|
||||
case .toot(let objectID, _):
|
||||
statusObjectIDs.append(objectID)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let backgroundManagedObjectContext = context.backgroundManagedObjectContext
|
||||
backgroundManagedObjectContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
for objectID in statusObjectIDs {
|
||||
let toot = backgroundManagedObjectContext.object(with: objectID) as! Toot
|
||||
guard let replyToID = toot.inReplyToID, toot.replyTo == nil else {
|
||||
// skip
|
||||
continue
|
||||
}
|
||||
self.context.statusPrefetchingService.prefetchReplyTo(
|
||||
domain: domain,
|
||||
statusObjectID: toot.objectID,
|
||||
statusID: toot.id,
|
||||
replyToStatusID: replyToID,
|
||||
authorizationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,4 +20,5 @@ protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewControl
|
|||
var managedObjectContext: NSManagedObjectContext { get }
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? { get }
|
||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item?
|
||||
func items(indexPaths: [IndexPath]) -> [Item]
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"Common.Controls.Status.ShowPost" = "Show Post";
|
||||
"Common.Controls.Status.StatusContentWarning" = "content warning";
|
||||
"Common.Controls.Status.UserBoosted" = "%@ boosted";
|
||||
"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
|
||||
"Common.Controls.Timeline.LoadMore" = "Load More";
|
||||
"Common.Countable.Photo.Multiple" = "photos";
|
||||
"Common.Countable.Photo.Single" = "photo";
|
||||
|
|
|
@ -70,4 +70,18 @@ extension HomeTimelineViewController: StatusProvider {
|
|||
return item
|
||||
}
|
||||
|
||||
func items(indexPaths: [IndexPath]) -> [Item] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
return []
|
||||
}
|
||||
|
||||
var items: [Item] = []
|
||||
for indexPath in indexPaths {
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue }
|
||||
items.append(item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ extension HomeTimelineViewController {
|
|||
viewModel.tableView = tableView
|
||||
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||
tableView.delegate = self
|
||||
tableView.prefetchDataSource = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self,
|
||||
|
@ -239,6 +240,13 @@ extension HomeTimelineViewController: UITableViewDelegate {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
extension HomeTimelineViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
handleTableView(tableView, prefetchRowsAt: indexPaths)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||
extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
|
||||
func navigationBar() -> UINavigationBar? {
|
||||
|
|
|
@ -375,7 +375,7 @@ extension MastodonPickServerViewController: UITableViewDelegate {
|
|||
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil }
|
||||
guard case let .server(server) = item else { return nil }
|
||||
guard case .server = item else { return nil }
|
||||
|
||||
if tableView.indexPathForSelectedRow == indexPath {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
|
|
|
@ -45,7 +45,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
|||
viewModel.context.apiService.servers(language: nil, category: nil)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
case .failure:
|
||||
// TODO: handle error
|
||||
stateMachine.enter(Fail.self)
|
||||
case .finished:
|
||||
|
@ -84,7 +84,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState {
|
|||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
guard let viewModel = self.viewModel, let stateMachine = self.stateMachine else { return }
|
||||
guard let viewModel = self.viewModel else { return }
|
||||
viewModel.isLoadingIndexedServers.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ class MastodonPickServerViewModel: NSObject {
|
|||
switch result {
|
||||
case .success(let response):
|
||||
self.unindexedServers.send(response.value)
|
||||
case .failure(let error):
|
||||
case .failure:
|
||||
// TODO: What should be presented when user inputs invalid search text?
|
||||
self.unindexedServers.send([])
|
||||
}
|
||||
|
|
|
@ -70,4 +70,18 @@ extension PublicTimelineViewController: StatusProvider {
|
|||
return item
|
||||
}
|
||||
|
||||
func items(indexPaths: [IndexPath]) -> [Item] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
return []
|
||||
}
|
||||
|
||||
var items: [Item] = []
|
||||
for indexPath in indexPaths {
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { continue }
|
||||
items.append(item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ extension PublicTimelineViewController {
|
|||
viewModel.tableView = tableView
|
||||
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||
tableView.delegate = self
|
||||
tableView.prefetchDataSource = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self,
|
||||
|
@ -125,6 +126,13 @@ extension PublicTimelineViewController: UITableViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
extension PublicTimelineViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
handleTableView(tableView, prefetchRowsAt: indexPaths)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||
extension PublicTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
|
||||
func navigationBar() -> UINavigationBar? {
|
||||
|
|
|
@ -24,6 +24,29 @@ final class StatusView: UIView {
|
|||
static let avatarImageCornerRadius: CGFloat = 4
|
||||
static let contentWarningBlurRadius: CGFloat = 12
|
||||
|
||||
static let boostIconImage: UIImage = {
|
||||
let font = UIFont.systemFont(ofSize: 13, weight: .medium)
|
||||
let configuration = UIImage.SymbolConfiguration(font: font)
|
||||
let image = UIImage(systemName: "arrow.2.squarepath", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color)
|
||||
return image
|
||||
}()
|
||||
|
||||
static let replyIconImage: UIImage = {
|
||||
let font = UIFont.systemFont(ofSize: 13, weight: .medium)
|
||||
let configuration = UIImage.SymbolConfiguration(font: font)
|
||||
let image = UIImage(systemName: "arrowshape.turn.up.left.fill", withConfiguration: configuration)!.withTintColor(Asset.Colors.Label.secondary.color)
|
||||
return image
|
||||
}()
|
||||
|
||||
static func iconAttributedString(image: UIImage) -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString()
|
||||
let imageTextAttachment = NSTextAttachment()
|
||||
let imageAttribute = NSAttributedString(attachment: imageTextAttachment)
|
||||
imageTextAttachment.image = image
|
||||
attributedString.append(imageAttribute)
|
||||
return attributedString
|
||||
}
|
||||
|
||||
weak var delegate: StatusViewDelegate?
|
||||
var isStatusTextSensitive = false
|
||||
var pollTableViewDataSource: UITableViewDiffableDataSource<PollSection, PollItem>?
|
||||
|
@ -33,14 +56,7 @@ final class StatusView: UIView {
|
|||
|
||||
let headerIconLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let attributedString = NSMutableAttributedString()
|
||||
let imageTextAttachment = NSTextAttachment()
|
||||
let font = UIFont.systemFont(ofSize: 13, weight: .medium)
|
||||
let configuration = UIImage.SymbolConfiguration(font: font)
|
||||
imageTextAttachment.image = UIImage(systemName: "arrow.2.squarepath", withConfiguration: configuration)?.withTintColor(Asset.Colors.Label.secondary.color)
|
||||
let imageAttribute = NSAttributedString(attachment: imageTextAttachment)
|
||||
attributedString.append(imageAttribute)
|
||||
label.attributedText = attributedString
|
||||
label.attributedText = StatusView.iconAttributedString(image: StatusView.boostIconImage)
|
||||
return label
|
||||
}()
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// APIService+Status.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import DateToolsSwift
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func status(
|
||||
domain: String,
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
return Mastodon.API.Statuses.status(
|
||||
session: session,
|
||||
domain: domain,
|
||||
statusID: statusID,
|
||||
authorization: authorization
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> in
|
||||
return APIService.Persist.persistToots(
|
||||
managedObjectContext: self.backgroundManagedObjectContext,
|
||||
domain: domain,
|
||||
query: nil,
|
||||
response: response.map { [$0] },
|
||||
persistType: .lookUp,
|
||||
requestMastodonUserID: nil,
|
||||
log: OSLog.api
|
||||
)
|
||||
.setFailureType(to: Error.self)
|
||||
.tryMap { result -> Mastodon.Response.Content<Mastodon.Entity.Status> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
|
@ -19,12 +19,13 @@ extension APIService.Persist {
|
|||
case `public`
|
||||
case home
|
||||
case likeList
|
||||
case lookUp
|
||||
}
|
||||
|
||||
static func persistToots(
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
domain: String,
|
||||
query: Mastodon.API.Timeline.TimelineQuery,
|
||||
query: Mastodon.API.Timeline.TimelineQuery?,
|
||||
response: Mastodon.Response.Content<[Mastodon.Entity.Status]>,
|
||||
persistType: PersistTimelineType,
|
||||
requestMastodonUserID: MastodonUser.ID?, // could be nil when response from public endpoint
|
||||
|
@ -122,6 +123,7 @@ extension APIService.Persist {
|
|||
case .home: return .homeTimeline
|
||||
case .public: return .publicTimeline
|
||||
case .likeList: return .likeList
|
||||
case .lookUp: return .lookUp
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -152,7 +154,8 @@ extension APIService.Persist {
|
|||
// home timeline tasks
|
||||
switch persistType {
|
||||
case .home:
|
||||
guard let requestMastodonUserID = requestMastodonUserID else {
|
||||
guard let query = query,
|
||||
let requestMastodonUserID = requestMastodonUserID else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// StatusPrefetchingService.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-10.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
final class StatusPrefetchingService {
|
||||
|
||||
typealias TaskID = String
|
||||
|
||||
let workingQueue = DispatchQueue(label: "status-prefetching-service-working-queue")
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:]
|
||||
|
||||
weak var apiService: APIService?
|
||||
|
||||
init(apiService: APIService) {
|
||||
self.apiService = apiService
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusPrefetchingService {
|
||||
|
||||
func prefetchReplyTo(
|
||||
domain: String,
|
||||
statusObjectID: NSManagedObjectID,
|
||||
statusID: Mastodon.Entity.Status.ID,
|
||||
replyToStatusID: Mastodon.Entity.Status.ID,
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) {
|
||||
workingQueue.async { [weak self] in
|
||||
guard let self = self, let apiService = self.apiService else { return }
|
||||
let taskID = domain + "@" + statusID + "->" + replyToStatusID
|
||||
guard self.statusPrefetchingDisposeBagDict[taskID] == nil else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: prefetching replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID)
|
||||
|
||||
self.statusPrefetchingDisposeBagDict[taskID] = apiService.status(
|
||||
domain: domain,
|
||||
statusID: replyToStatusID,
|
||||
authorizationBox: authorizationBox
|
||||
)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
// remove task when completed
|
||||
guard let self = self else { return }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: prefeched replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID)
|
||||
self.statusPrefetchingDisposeBagDict[taskID] = nil
|
||||
}, receiveValue: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let backgroundManagedObjectContext = apiService.backgroundManagedObjectContext
|
||||
backgroundManagedObjectContext.performChanges {
|
||||
guard let status = backgroundManagedObjectContext.object(with: statusObjectID) as? Toot else { return }
|
||||
do {
|
||||
let predicate = Toot.predicate(domain: domain, id: replyToStatusID)
|
||||
let request = Toot.sortedFetchRequest
|
||||
request.predicate = predicate
|
||||
request.returnsObjectsAsFaults = false
|
||||
request.fetchLimit = 1
|
||||
guard let replyTo = try backgroundManagedObjectContext.fetch(request).first else { return }
|
||||
status.update(replyTo: replyTo)
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
.sink { _ in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update status replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID)
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -24,6 +24,8 @@ class AppContext: ObservableObject {
|
|||
let apiService: APIService
|
||||
let authenticationService: AuthenticationService
|
||||
|
||||
let statusPrefetchingService: StatusPrefetchingService
|
||||
|
||||
let documentStore: DocumentStore
|
||||
private var documentStoreSubscription: AnyCancellable!
|
||||
|
||||
|
@ -46,6 +48,10 @@ class AppContext: ObservableObject {
|
|||
apiService: _apiService
|
||||
)
|
||||
|
||||
statusPrefetchingService = StatusPrefetchingService(
|
||||
apiService: _apiService
|
||||
)
|
||||
|
||||
documentStore = DocumentStore()
|
||||
documentStoreSubscription = documentStore.objectWillChange
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// Mastodon+API+Statuses.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Statuses {
|
||||
|
||||
static func viewStatusEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL {
|
||||
let pathComponent = "statuses/" + statusID
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// View specific status
|
||||
///
|
||||
/// View information about a status
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/3/10
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/statuses/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - statusID: id for status
|
||||
/// - authorization: User token. Could be nil if status is public
|
||||
/// - Returns: `AnyPublisher` contains `Status` nested in the response
|
||||
public static func status(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
statusID: Mastodon.Entity.Poll.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Status>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: viewStatusEndpointURL(domain: domain, statusID: statusID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
|
@ -95,6 +95,7 @@ extension Mastodon.API {
|
|||
public enum OAuth { }
|
||||
public enum Onboarding { }
|
||||
public enum Polls { }
|
||||
public enum Statuses { }
|
||||
public enum Timeline { }
|
||||
public enum Favorites { }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue