Merge branch 'feature/prefetching' into fix/search
This commit is contained in:
commit
10c2b57b79
|
@ -239,7 +239,6 @@
|
|||
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
|
||||
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
|
||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44384E25E8C1FA008912A2 /* CALayer.swift */; };
|
||||
DB443CD22694326A00159B29 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DB443CD0269415D200159B29 /* Localizable.stringsdict */; };
|
||||
DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB443CD32694627B00159B29 /* AppearanceView.swift */; };
|
||||
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; };
|
||||
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */; };
|
||||
|
@ -284,6 +283,8 @@
|
|||
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; };
|
||||
DB52D33A26839DD800D43133 /* ImageTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB52D33926839DD800D43133 /* ImageTask.swift */; };
|
||||
DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */; };
|
||||
DB564BD0269F2F83001E39A7 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */; };
|
||||
DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */; };
|
||||
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
|
||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
|
||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
|
||||
|
@ -885,8 +886,6 @@
|
|||
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
|
||||
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = "<group>"; };
|
||||
DB443CCF269415D200159B29 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
DB443CD1269415D800159B29 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = "<group>"; };
|
||||
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = "<group>"; };
|
||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerSection.swift; sourceTree = "<group>"; };
|
||||
|
@ -931,6 +930,9 @@
|
|||
DB51D171262832380062B7A1 /* BlurHashEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = "<group>"; };
|
||||
DB52D33926839DD800D43133 /* ImageTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTask.swift; sourceTree = "<group>"; };
|
||||
DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TwitterTextEditor+String.swift"; sourceTree = "<group>"; };
|
||||
DB564BCF269F2F83001E39A7 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
DB564BD1269F2F8A001E39A7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFilterService.swift; sourceTree = "<group>"; };
|
||||
DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
|
||||
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = "<group>"; };
|
||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1585,6 +1587,7 @@
|
|||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
||||
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
|
||||
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1858,7 +1861,7 @@
|
|||
164F0EBB267D4FE400249499 /* BoopSound.caf */,
|
||||
DB427DDE25BAA00100D1B89D /* Assets.xcassets */,
|
||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */,
|
||||
DB443CD0269415D200159B29 /* Localizable.stringsdict */,
|
||||
DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */,
|
||||
DB3D100F25BAA75E00EAA174 /* Localizable.strings */,
|
||||
DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */,
|
||||
);
|
||||
|
@ -3013,7 +3016,7 @@
|
|||
files = (
|
||||
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */,
|
||||
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */,
|
||||
DB443CD22694326A00159B29 /* Localizable.stringsdict in Resources */,
|
||||
DB564BD0269F2F83001E39A7 /* Localizable.stringsdict in Resources */,
|
||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */,
|
||||
DB427DDF25BAA00100D1B89D /* Assets.xcassets in Resources */,
|
||||
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */,
|
||||
|
@ -3311,6 +3314,7 @@
|
|||
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
|
||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||
DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */,
|
||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */,
|
||||
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
|
||||
|
@ -3842,15 +3846,14 @@
|
|||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB443CD0269415D200159B29 /* Localizable.stringsdict */ = {
|
||||
DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
DB443CCF269415D200159B29 /* en */,
|
||||
DB443CD1269415D800159B29 /* ar */,
|
||||
DB564BCF269F2F83001E39A7 /* ar */,
|
||||
DB564BD1269F2F8A001E39A7 /* en */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
path = /Users/mainasuk/Developer/Mastodon/Mastodon/Resources;
|
||||
sourceTree = "<absolute>";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
|
|
|
@ -12,12 +12,12 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>20</integer>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>21</integer>
|
||||
<integer>19</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -167,3 +167,25 @@ extension Item: Hashable {
|
|||
}
|
||||
|
||||
extension Item: Differentiable { }
|
||||
|
||||
extension Item {
|
||||
var statusObjectItem: StatusObjectItem? {
|
||||
switch self {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
return .homeTimelineIndex(objectID: objectID)
|
||||
case .root(let objectID, _),
|
||||
.reply(let objectID, _),
|
||||
.leaf(let objectID, _),
|
||||
.status(let objectID, _),
|
||||
.reportStatus(let objectID, _):
|
||||
return .status(objectID: objectID)
|
||||
case .leafBottomLoader,
|
||||
.homeMiddleLoader,
|
||||
.publicMiddleLoader,
|
||||
.topLoader,
|
||||
.bottomLoader,
|
||||
.emptyStateHeader:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,3 +37,14 @@ extension NotificationItem: Hashable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationItem {
|
||||
var statusObjectItem: StatusObjectItem? {
|
||||
switch self {
|
||||
case .notification(let objectID, _):
|
||||
return .mastodonNotification(objectID: objectID)
|
||||
case .bottomLoader:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,3 +71,18 @@ extension SearchResultItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchResultItem {
|
||||
var statusObjectItem: StatusObjectItem? {
|
||||
switch self {
|
||||
case .status(let objectID, _):
|
||||
return .status(objectID: objectID)
|
||||
case .hashtag,
|
||||
.account,
|
||||
.accountObjectID,
|
||||
.hashtagObjectID,
|
||||
.bottomLoader:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// StatusFilterService.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-7-14.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
final class StatusFilterService {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
weak var apiService: APIService?
|
||||
weak var authenticationService: AuthenticationService?
|
||||
let filterUpdatePublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
let activeFilters = CurrentValueSubject<[Mastodon.Entity.Filter], Never>([])
|
||||
|
||||
init(
|
||||
apiService: APIService,
|
||||
authenticationService: AuthenticationService
|
||||
) {
|
||||
self.apiService = apiService
|
||||
self.authenticationService = authenticationService
|
||||
|
||||
// fetch account filters every 300s
|
||||
// also trigger fetch when app resume from background
|
||||
let filterUpdateTimerPublisher = Timer.publish(every: 300.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
filterUpdateTimerPublisher
|
||||
.map { _ in }
|
||||
.subscribe(filterUpdatePublisher)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
let activeMastodonAuthenticationBox = authenticationService.activeMastodonAuthenticationBox
|
||||
Publishers.CombineLatest(
|
||||
activeMastodonAuthenticationBox,
|
||||
filterUpdatePublisher
|
||||
)
|
||||
.flatMap { box, _ -> AnyPublisher<Result<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error>, Never> in
|
||||
guard let box = box else {
|
||||
return Just(Result { throw APIService.APIError.implicit(.authenticationMissing) }).eraseToAnyPublisher()
|
||||
}
|
||||
return apiService.filters(mastodonAuthenticationBox: box)
|
||||
.map { response in
|
||||
let now = Date()
|
||||
let newResponse = response.map { filters in
|
||||
return filters.filter { $0.expiresAt > now } // filter out expired rules
|
||||
}
|
||||
return Result<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error>.success(newResponse)
|
||||
}
|
||||
.catch { error in
|
||||
Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error>.failure(error))
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters success. %ld items", ((#file as NSString).lastPathComponent), #line, #function, response.value.count)
|
||||
self.activeFilters.value = response.value
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// make initial trigger once
|
||||
filterUpdatePublisher.send()
|
||||
}
|
||||
|
||||
}
|
|
@ -60,6 +60,8 @@ extension StatusSection {
|
|||
}
|
||||
#endif
|
||||
|
||||
static let logger = Logger(subsystem: "StatusSection", category: "logic")
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
timelineContext: TimelineContext,
|
||||
|
@ -248,7 +250,8 @@ extension StatusSection {
|
|||
timelineContext: TimelineContext
|
||||
) -> AnyPublisher<Bool, Never> {
|
||||
guard let content = content,
|
||||
let currentFilterContext = timelineContext.filterContext else {
|
||||
let currentFilterContext = timelineContext.filterContext,
|
||||
!filters.isEmpty else {
|
||||
return Just(false).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
@ -352,18 +355,29 @@ extension StatusSection {
|
|||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
let content: MastodonMetaContent? = {
|
||||
if let operation = dependency.context.statusPrefetchingService.statusContentOperations.removeValue(forKey: status.objectID),
|
||||
let result = operation.result {
|
||||
switch result {
|
||||
case .success(let content): return content
|
||||
case .failure: return nil
|
||||
}
|
||||
} else {
|
||||
let document = MastodonContent(
|
||||
content: (status.reblog ?? status).content,
|
||||
emojis: (status.reblog ?? status).emojiMeta
|
||||
)
|
||||
let content = try? MastodonMetaContent.convert(document: document)
|
||||
return try? MastodonMetaContent.convert(document: document)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
if status.author.id == requestUserID || status.reblog?.author.id == requestUserID {
|
||||
// do not filter myself
|
||||
} else {
|
||||
let needsFilter = StatusSection.needsFilterStatus(
|
||||
content: content,
|
||||
filters: AppContext.shared.authenticationService.activeFilters.value,
|
||||
filters: AppContext.shared.statusFilterService.activeFilters.value,
|
||||
timelineContext: timelineContext
|
||||
)
|
||||
needsFilter
|
||||
|
@ -1130,3 +1144,44 @@ extension StatusSection {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
class StatusContentOperation: Operation {
|
||||
|
||||
let logger = Logger(subsystem: "StatusContentOperation", category: "logic")
|
||||
|
||||
// input
|
||||
let statusObjectID: NSManagedObjectID
|
||||
let mastodonContent: MastodonContent
|
||||
|
||||
// output
|
||||
var result: Result<MastodonMetaContent, Error>?
|
||||
|
||||
init(
|
||||
statusObjectID: NSManagedObjectID,
|
||||
mastodonContent: MastodonContent
|
||||
) {
|
||||
self.statusObjectID = statusObjectID
|
||||
self.mastodonContent = mastodonContent
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func main() {
|
||||
guard !isCancelled else { return }
|
||||
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): prcoess \(self.statusObjectID)…")
|
||||
|
||||
do {
|
||||
let content = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
result = .success(content)
|
||||
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): process success \(self.statusObjectID)")
|
||||
} catch {
|
||||
result = .failure(error)
|
||||
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): process fail \(self.statusObjectID)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): cancel \(self.statusObjectID.debugDescription)")
|
||||
super.cancel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ import CoreDataStack
|
|||
|
||||
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||
func handleTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
let statusObjectItems = self.statusObjectItems(indexPaths: indexPaths)
|
||||
self.context.statusPrefetchingService.prefetch(statusObjectItems: statusObjectItems)
|
||||
|
||||
// prefetch reply status
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
|
@ -47,4 +50,9 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
} // end for in
|
||||
} // end context.perform
|
||||
} // end func
|
||||
|
||||
func handleTableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
let statusObjectItems = self.statusObjectItems(indexPaths: indexPaths)
|
||||
self.context.statusPrefetchingService.cancelPrefetch(statusObjectItems: statusObjectItems)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,10 +22,16 @@ protocol StatusProvider: NeedsDependency & DisposeBagCollectable & UIViewControl
|
|||
|
||||
// sync
|
||||
var managedObjectContext: NSManagedObjectContext { get }
|
||||
|
||||
@available(*, deprecated)
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? { get }
|
||||
@available(*, deprecated)
|
||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item?
|
||||
@available(*, deprecated)
|
||||
func items(indexPaths: [IndexPath]) -> [Item]
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem]
|
||||
|
||||
#if ASDK
|
||||
func status(node: ASCellNode?, indexPath: IndexPath?) -> Status?
|
||||
#endif
|
||||
|
@ -38,3 +44,9 @@ extension StatusProvider {
|
|||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
enum StatusObjectItem {
|
||||
case status(objectID: NSManagedObjectID)
|
||||
case homeTimelineIndex(objectID: NSManagedObjectID)
|
||||
case mastodonNotification(objectID: NSManagedObjectID) // may not contains status
|
||||
}
|
||||
|
|
|
@ -10,12 +10,12 @@ import AVKit
|
|||
import GameController
|
||||
|
||||
// Check List Last Updated
|
||||
// - HomeViewController: 2021/4/30
|
||||
// - HomeViewController: 2021/7/15
|
||||
// - FavoriteViewController: 2021/4/30
|
||||
// - HashtagTimelineViewController: 2021/4/30
|
||||
// - UserTimelineViewController: 2021/4/30
|
||||
// - ThreadViewController: 2021/4/30
|
||||
// * StatusTableViewControllerAspect: 2021/4/30
|
||||
// * StatusTableViewControllerAspect: 2021/7/15
|
||||
|
||||
// (Fake) Aspect protocol to group common protocol extension implementations
|
||||
// Needs update related view controller when aspect interface changes
|
||||
|
@ -146,12 +146,20 @@ extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegat
|
|||
|
||||
// [C1] aspectTableView(:prefetchRowsAt)
|
||||
extension StatusTableViewControllerAspect where Self: UITableViewDataSourcePrefetching & StatusTableViewCellDelegate & StatusProvider {
|
||||
/// [Data Source] hook to prefetch reply to info for status
|
||||
/// [Data Source] hook to prefetch status
|
||||
func aspectTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
handleTableView(tableView, prefetchRowsAt: indexPaths)
|
||||
}
|
||||
}
|
||||
|
||||
// [C2] aspectTableView(:prefetchRowsAt)
|
||||
extension StatusTableViewControllerAspect where Self: UITableViewDataSourcePrefetching & StatusTableViewCellDelegate & StatusProvider {
|
||||
/// [Data Source] hook to cancel prefetch status
|
||||
func aspectTableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
handleTableView(tableView, cancelPrefetchingForRowsAt: indexPaths)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate & NeedsDependency [D]
|
||||
|
||||
// [D1] aspectPlayerViewController(_:willBeginFullScreenPresentationWithAnimationCoordinator:)
|
||||
|
|
|
@ -84,6 +84,12 @@ extension HashtagTimelineViewController: StatusProvider {
|
|||
return items
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HashtagTimelineViewController: UserProvider {}
|
||||
|
|
|
@ -84,6 +84,12 @@ extension HomeTimelineViewController: StatusProvider {
|
|||
return items
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HomeTimelineViewController: UserProvider {}
|
||||
|
|
|
@ -431,6 +431,10 @@ extension HomeTimelineViewController: UITableViewDataSourcePrefetching {
|
|||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
aspectTableView(tableView, prefetchRowsAt: indexPaths)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||
|
|
|
@ -61,5 +61,10 @@ extension NotificationViewController: StatusProvider {
|
|||
return []
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -84,6 +84,12 @@ extension FavoriteViewController: StatusProvider {
|
|||
return items
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FavoriteViewController: UserProvider {}
|
||||
|
|
|
@ -84,6 +84,12 @@ extension UserTimelineViewController: StatusProvider {
|
|||
return items
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UserTimelineViewController: UserProvider {}
|
||||
|
|
|
@ -84,6 +84,12 @@ extension PublicTimelineViewController: StatusProvider {
|
|||
return items
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PublicTimelineViewController: UserProvider {}
|
||||
|
|
|
@ -64,6 +64,12 @@ extension SearchResultViewController: StatusProvider {
|
|||
return []
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SearchResultViewController: UserProvider {}
|
||||
|
|
|
@ -85,6 +85,12 @@ extension ThreadViewController: StatusProvider {
|
|||
return items
|
||||
}
|
||||
|
||||
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem] {
|
||||
guard let diffableDataSource = self.viewModel.diffableDataSource else { return [] }
|
||||
let items = indexPaths.compactMap { diffableDataSource.itemIdentifier(for: $0)?.statusObjectItem }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ThreadViewController: UserProvider {}
|
||||
|
|
|
@ -27,7 +27,6 @@ final class AuthenticationService: NSObject {
|
|||
let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([])
|
||||
let activeMastodonAuthentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil)
|
||||
let activeMastodonAuthenticationBox = CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>(nil)
|
||||
let activeFilters = CurrentValueSubject<[Mastodon.Entity.Filter], Never>([])
|
||||
|
||||
init(
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
|
@ -88,53 +87,6 @@ final class AuthenticationService: NSObject {
|
|||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
|
||||
// fetch account filters every 60s and filter out expired items
|
||||
let filterUpdateTimerPublisher = Timer.publish(every: 60.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
let filterUpdatePublisher = PassthroughSubject<Void, Never>()
|
||||
|
||||
filterUpdateTimerPublisher
|
||||
.map { _ in }
|
||||
.subscribe(filterUpdatePublisher)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
activeMastodonAuthenticationBox,
|
||||
filterUpdatePublisher
|
||||
)
|
||||
.flatMap { box, _ -> AnyPublisher<Result<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error>, Never> in
|
||||
guard let box = box else {
|
||||
return Just(Result { throw APIService.APIError.implicit(.authenticationMissing) }).eraseToAnyPublisher()
|
||||
}
|
||||
return apiService.filters(mastodonAuthenticationBox: box)
|
||||
.map { response in
|
||||
let now = Date()
|
||||
let newResponse = response.map { filters in
|
||||
return filters.filter { $0.expiresAt > now }
|
||||
}
|
||||
return Result<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error>.success(newResponse)
|
||||
}
|
||||
.catch { error in
|
||||
Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Filter]>, Error>.failure(error))
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.sink { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters success. %ld items", ((#file as NSString).lastPathComponent), #line, #function, response.value.count)
|
||||
self.activeFilters.value = response.value
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch account filters fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
filterUpdatePublisher.send()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,22 +11,90 @@ import Combine
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
|
||||
final class StatusPrefetchingService {
|
||||
|
||||
typealias TaskID = String
|
||||
typealias StatusObjectID = NSManagedObjectID
|
||||
|
||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPrefetchingService.working-queue")
|
||||
|
||||
// StatusContentOperation
|
||||
let statusContentOperationQueue: OperationQueue = {
|
||||
let queue = OperationQueue()
|
||||
queue.name = "org.joinmastodon.app.StatusPrefetchingService.statusContentOperationQueue"
|
||||
queue.maxConcurrentOperationCount = 2
|
||||
return queue
|
||||
}()
|
||||
var statusContentOperations: [StatusObjectID: StatusContentOperation] = [:]
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:]
|
||||
|
||||
// input
|
||||
weak var apiService: APIService?
|
||||
let managedObjectContext: NSManagedObjectContext
|
||||
let backgroundManagedObjectContext: NSManagedObjectContext // read-only
|
||||
|
||||
init(apiService: APIService) {
|
||||
init(
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
backgroundManagedObjectContext: NSManagedObjectContext,
|
||||
apiService: APIService
|
||||
) {
|
||||
self.managedObjectContext = managedObjectContext
|
||||
self.backgroundManagedObjectContext = backgroundManagedObjectContext
|
||||
self.apiService = apiService
|
||||
}
|
||||
|
||||
private func status(from statusObjectItem: StatusObjectItem) -> Status? {
|
||||
assert(Thread.isMainThread)
|
||||
switch statusObjectItem {
|
||||
case .homeTimelineIndex(let objectID):
|
||||
let homeTimelineIndex = try? managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex
|
||||
return homeTimelineIndex?.status
|
||||
case .mastodonNotification(let objectID):
|
||||
let mastodonNotification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification
|
||||
return mastodonNotification?.status
|
||||
case .status(let objectID):
|
||||
let status = try? managedObjectContext.existingObject(with: objectID) as? Status
|
||||
return status
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusPrefetchingService {
|
||||
func prefetch(statusObjectItems items: [StatusObjectItem]) {
|
||||
for item in items {
|
||||
guard let status = status(from: item), !status.isDeleted else { continue }
|
||||
|
||||
// status content parser task
|
||||
if statusContentOperations[status.objectID] == nil {
|
||||
let mastodonContent = MastodonContent(
|
||||
content: (status.reblog ?? status).content,
|
||||
emojis: (status.reblog ?? status).emojiMeta
|
||||
)
|
||||
let operation = StatusContentOperation(
|
||||
statusObjectID: status.objectID,
|
||||
mastodonContent: mastodonContent
|
||||
)
|
||||
statusContentOperations[status.objectID] = operation
|
||||
statusContentOperationQueue.addOperation(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelPrefetch(statusObjectItems items: [StatusObjectItem]) {
|
||||
for item in items {
|
||||
guard let status = status(from: item), !status.isDeleted else { continue }
|
||||
|
||||
// cancel status content parser task
|
||||
statusContentOperations.removeValue(forKey: status.objectID)?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StatusPrefetchingService {
|
||||
|
|
|
@ -33,6 +33,7 @@ class AppContext: ObservableObject {
|
|||
let settingService: SettingService
|
||||
|
||||
let blockDomainService: BlockDomainService
|
||||
let statusFilterService: StatusFilterService
|
||||
let photoLibraryService = PhotoLibraryService()
|
||||
|
||||
let placeholderImageCacheService = PlaceholderImageCacheService()
|
||||
|
@ -69,7 +70,10 @@ class AppContext: ObservableObject {
|
|||
emojiService = EmojiService(
|
||||
apiService: apiService
|
||||
)
|
||||
|
||||
statusPrefetchingService = StatusPrefetchingService(
|
||||
managedObjectContext: _managedObjectContext,
|
||||
backgroundManagedObjectContext: _backgroundManagedObjectContext,
|
||||
apiService: _apiService
|
||||
)
|
||||
let _notificationService = NotificationService(
|
||||
|
@ -89,6 +93,11 @@ class AppContext: ObservableObject {
|
|||
authenticationService: _authenticationService
|
||||
)
|
||||
|
||||
statusFilterService = StatusFilterService(
|
||||
apiService: _apiService,
|
||||
authenticationService: _authenticationService
|
||||
)
|
||||
|
||||
documentStore = DocumentStore()
|
||||
documentStoreSubscription = documentStore.objectWillChange
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
|
|
@ -81,6 +81,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// reset notification badge
|
||||
UserDefaults.shared.notificationBadgeCount = 0
|
||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||
|
||||
// trigger status filter update
|
||||
AppContext.shared.statusFilterService.filterUpdatePublisher.send()
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
|
|
Loading…
Reference in New Issue