feat: add reply to header for toot

This commit is contained in:
CMK 2021-03-10 19:12:53 +08:00
parent 807dfd9ea7
commit 75d39aabf0
23 changed files with 392 additions and 22 deletions

View File

@ -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) {

View File

@ -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",

View File

@ -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 */,

View File

@ -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?,

View File

@ -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")

View File

@ -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

View File

@ -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
)
}
}
}
}

View File

@ -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]
}

View File

@ -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";

View File

@ -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
}
}

View File

@ -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? {

View File

@ -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)

View File

@ -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
}
}

View File

@ -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([])
}

View File

@ -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
}
}

View File

@ -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? {

View File

@ -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
}()

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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)
})
}
}
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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 { }
}