forked from zelo72/mastodon-ios
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 */; };
|
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
|
||||||
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
|
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
|
||||||
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44384E25E8C1FA008912A2 /* CALayer.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 */; };
|
DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB443CD32694627B00159B29 /* AppearanceView.swift */; };
|
||||||
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; };
|
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; };
|
||||||
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.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 */; };
|
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; };
|
||||||
DB52D33A26839DD800D43133 /* ImageTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB52D33926839DD800D43133 /* ImageTask.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 */; };
|
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 */; };
|
DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; };
|
||||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1585,6 +1587,7 @@
|
||||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||||
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
||||||
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
|
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
|
||||||
|
DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */,
|
||||||
);
|
);
|
||||||
path = Section;
|
path = Section;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1858,7 +1861,7 @@
|
||||||
164F0EBB267D4FE400249499 /* BoopSound.caf */,
|
164F0EBB267D4FE400249499 /* BoopSound.caf */,
|
||||||
DB427DDE25BAA00100D1B89D /* Assets.xcassets */,
|
DB427DDE25BAA00100D1B89D /* Assets.xcassets */,
|
||||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */,
|
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */,
|
||||||
DB443CD0269415D200159B29 /* Localizable.stringsdict */,
|
DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */,
|
||||||
DB3D100F25BAA75E00EAA174 /* Localizable.strings */,
|
DB3D100F25BAA75E00EAA174 /* Localizable.strings */,
|
||||||
DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */,
|
DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */,
|
||||||
);
|
);
|
||||||
|
@ -3013,7 +3016,7 @@
|
||||||
files = (
|
files = (
|
||||||
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */,
|
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */,
|
||||||
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */,
|
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */,
|
||||||
DB443CD22694326A00159B29 /* Localizable.stringsdict in Resources */,
|
DB564BD0269F2F83001E39A7 /* Localizable.stringsdict in Resources */,
|
||||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */,
|
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */,
|
||||||
DB427DDF25BAA00100D1B89D /* Assets.xcassets in Resources */,
|
DB427DDF25BAA00100D1B89D /* Assets.xcassets in Resources */,
|
||||||
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */,
|
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */,
|
||||||
|
@ -3311,6 +3314,7 @@
|
||||||
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
|
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
|
||||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||||
|
DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */,
|
||||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||||
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */,
|
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */,
|
||||||
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
|
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
|
||||||
|
@ -3842,15 +3846,14 @@
|
||||||
name = LaunchScreen.storyboard;
|
name = LaunchScreen.storyboard;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
DB443CD0269415D200159B29 /* Localizable.stringsdict */ = {
|
DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
children = (
|
children = (
|
||||||
DB443CCF269415D200159B29 /* en */,
|
DB564BCF269F2F83001E39A7 /* ar */,
|
||||||
DB443CD1269415D800159B29 /* ar */,
|
DB564BD1269F2F8A001E39A7 /* en */,
|
||||||
);
|
);
|
||||||
name = Localizable.stringsdict;
|
name = Localizable.stringsdict;
|
||||||
path = /Users/mainasuk/Developer/Mastodon/Mastodon/Resources;
|
sourceTree = "<group>";
|
||||||
sourceTree = "<absolute>";
|
|
||||||
};
|
};
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,12 @@
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>20</integer>
|
<integer>21</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>3</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>21</integer>
|
<integer>19</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|
|
@ -167,3 +167,25 @@ extension Item: Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Item: Differentiable { }
|
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
|
#endif
|
||||||
|
|
||||||
|
static let logger = Logger(subsystem: "StatusSection", category: "logic")
|
||||||
|
|
||||||
static func tableViewDiffableDataSource(
|
static func tableViewDiffableDataSource(
|
||||||
for tableView: UITableView,
|
for tableView: UITableView,
|
||||||
timelineContext: TimelineContext,
|
timelineContext: TimelineContext,
|
||||||
|
@ -248,7 +250,8 @@ extension StatusSection {
|
||||||
timelineContext: TimelineContext
|
timelineContext: TimelineContext
|
||||||
) -> AnyPublisher<Bool, Never> {
|
) -> AnyPublisher<Bool, Never> {
|
||||||
guard let content = content,
|
guard let content = content,
|
||||||
let currentFilterContext = timelineContext.filterContext else {
|
let currentFilterContext = timelineContext.filterContext,
|
||||||
|
!filters.isEmpty else {
|
||||||
return Just(false).eraseToAnyPublisher()
|
return Just(false).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,18 +355,29 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.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(
|
let document = MastodonContent(
|
||||||
content: (status.reblog ?? status).content,
|
content: (status.reblog ?? status).content,
|
||||||
emojis: (status.reblog ?? status).emojiMeta
|
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 {
|
if status.author.id == requestUserID || status.reblog?.author.id == requestUserID {
|
||||||
// do not filter myself
|
// do not filter myself
|
||||||
} else {
|
} else {
|
||||||
let needsFilter = StatusSection.needsFilterStatus(
|
let needsFilter = StatusSection.needsFilterStatus(
|
||||||
content: content,
|
content: content,
|
||||||
filters: AppContext.shared.authenticationService.activeFilters.value,
|
filters: AppContext.shared.statusFilterService.activeFilters.value,
|
||||||
timelineContext: timelineContext
|
timelineContext: timelineContext
|
||||||
)
|
)
|
||||||
needsFilter
|
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 {
|
extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
func handleTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func handleTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
|
let statusObjectItems = self.statusObjectItems(indexPaths: indexPaths)
|
||||||
|
self.context.statusPrefetchingService.prefetch(statusObjectItems: statusObjectItems)
|
||||||
|
|
||||||
// prefetch reply status
|
// prefetch reply status
|
||||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||||
let domain = activeMastodonAuthenticationBox.domain
|
let domain = activeMastodonAuthenticationBox.domain
|
||||||
|
@ -47,4 +50,9 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
||||||
} // end for in
|
} // end for in
|
||||||
} // end context.perform
|
} // end context.perform
|
||||||
} // end func
|
} // 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
|
// sync
|
||||||
var managedObjectContext: NSManagedObjectContext { get }
|
var managedObjectContext: NSManagedObjectContext { get }
|
||||||
|
|
||||||
|
@available(*, deprecated)
|
||||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? { get }
|
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? { get }
|
||||||
|
@available(*, deprecated)
|
||||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item?
|
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item?
|
||||||
|
@available(*, deprecated)
|
||||||
func items(indexPaths: [IndexPath]) -> [Item]
|
func items(indexPaths: [IndexPath]) -> [Item]
|
||||||
|
|
||||||
|
func statusObjectItems(indexPaths: [IndexPath]) -> [StatusObjectItem]
|
||||||
|
|
||||||
#if ASDK
|
#if ASDK
|
||||||
func status(node: ASCellNode?, indexPath: IndexPath?) -> Status?
|
func status(node: ASCellNode?, indexPath: IndexPath?) -> Status?
|
||||||
#endif
|
#endif
|
||||||
|
@ -38,3 +44,9 @@ extension StatusProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
import GameController
|
||||||
|
|
||||||
// Check List Last Updated
|
// Check List Last Updated
|
||||||
// - HomeViewController: 2021/4/30
|
// - HomeViewController: 2021/7/15
|
||||||
// - FavoriteViewController: 2021/4/30
|
// - FavoriteViewController: 2021/4/30
|
||||||
// - HashtagTimelineViewController: 2021/4/30
|
// - HashtagTimelineViewController: 2021/4/30
|
||||||
// - UserTimelineViewController: 2021/4/30
|
// - UserTimelineViewController: 2021/4/30
|
||||||
// - ThreadViewController: 2021/4/30
|
// - ThreadViewController: 2021/4/30
|
||||||
// * StatusTableViewControllerAspect: 2021/4/30
|
// * StatusTableViewControllerAspect: 2021/7/15
|
||||||
|
|
||||||
// (Fake) Aspect protocol to group common protocol extension implementations
|
// (Fake) Aspect protocol to group common protocol extension implementations
|
||||||
// Needs update related view controller when aspect interface changes
|
// Needs update related view controller when aspect interface changes
|
||||||
|
@ -146,12 +146,20 @@ extension StatusTableViewControllerAspect where Self: StatusTableViewCellDelegat
|
||||||
|
|
||||||
// [C1] aspectTableView(:prefetchRowsAt)
|
// [C1] aspectTableView(:prefetchRowsAt)
|
||||||
extension StatusTableViewControllerAspect where Self: UITableViewDataSourcePrefetching & StatusTableViewCellDelegate & StatusProvider {
|
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]) {
|
func aspectTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
handleTableView(tableView, prefetchRowsAt: indexPaths)
|
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]
|
// MARK: - AVPlayerViewControllerDelegate & NeedsDependency [D]
|
||||||
|
|
||||||
// [D1] aspectPlayerViewController(_:willBeginFullScreenPresentationWithAnimationCoordinator:)
|
// [D1] aspectPlayerViewController(_:willBeginFullScreenPresentationWithAnimationCoordinator:)
|
||||||
|
|
|
@ -84,6 +84,12 @@ extension HashtagTimelineViewController: StatusProvider {
|
||||||
return items
|
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 {}
|
extension HashtagTimelineViewController: UserProvider {}
|
||||||
|
|
|
@ -84,6 +84,12 @@ extension HomeTimelineViewController: StatusProvider {
|
||||||
return items
|
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 {}
|
extension HomeTimelineViewController: UserProvider {}
|
||||||
|
|
|
@ -431,6 +431,10 @@ extension HomeTimelineViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
aspectTableView(tableView, prefetchRowsAt: indexPaths)
|
aspectTableView(tableView, prefetchRowsAt: indexPaths)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
|
aspectTableView(tableView, cancelPrefetchingForRowsAt: indexPaths)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||||
|
|
|
@ -61,5 +61,10 @@ extension NotificationViewController: StatusProvider {
|
||||||
return []
|
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
|
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 {}
|
extension FavoriteViewController: UserProvider {}
|
||||||
|
|
|
@ -84,6 +84,12 @@ extension UserTimelineViewController: StatusProvider {
|
||||||
return items
|
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 {}
|
extension UserTimelineViewController: UserProvider {}
|
||||||
|
|
|
@ -84,6 +84,12 @@ extension PublicTimelineViewController: StatusProvider {
|
||||||
return items
|
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 {}
|
extension PublicTimelineViewController: UserProvider {}
|
||||||
|
|
|
@ -64,6 +64,12 @@ extension SearchResultViewController: StatusProvider {
|
||||||
return []
|
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 {}
|
extension SearchResultViewController: UserProvider {}
|
||||||
|
|
|
@ -85,6 +85,12 @@ extension ThreadViewController: StatusProvider {
|
||||||
return items
|
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 {}
|
extension ThreadViewController: UserProvider {}
|
||||||
|
|
|
@ -27,7 +27,6 @@ final class AuthenticationService: NSObject {
|
||||||
let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([])
|
let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([])
|
||||||
let activeMastodonAuthentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil)
|
let activeMastodonAuthentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil)
|
||||||
let activeMastodonAuthenticationBox = CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>(nil)
|
let activeMastodonAuthenticationBox = CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>(nil)
|
||||||
let activeFilters = CurrentValueSubject<[Mastodon.Entity.Filter], Never>([])
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
managedObjectContext: NSManagedObjectContext,
|
managedObjectContext: NSManagedObjectContext,
|
||||||
|
@ -88,53 +87,6 @@ final class AuthenticationService: NSObject {
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure(error.localizedDescription)
|
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 CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
import MastodonMeta
|
||||||
|
|
||||||
final class StatusPrefetchingService {
|
final class StatusPrefetchingService {
|
||||||
|
|
||||||
typealias TaskID = String
|
typealias TaskID = String
|
||||||
|
typealias StatusObjectID = NSManagedObjectID
|
||||||
|
|
||||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPrefetchingService.working-queue")
|
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>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:]
|
private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:]
|
||||||
|
|
||||||
|
// input
|
||||||
weak var apiService: APIService?
|
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
|
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 {
|
extension StatusPrefetchingService {
|
||||||
|
|
|
@ -33,6 +33,7 @@ class AppContext: ObservableObject {
|
||||||
let settingService: SettingService
|
let settingService: SettingService
|
||||||
|
|
||||||
let blockDomainService: BlockDomainService
|
let blockDomainService: BlockDomainService
|
||||||
|
let statusFilterService: StatusFilterService
|
||||||
let photoLibraryService = PhotoLibraryService()
|
let photoLibraryService = PhotoLibraryService()
|
||||||
|
|
||||||
let placeholderImageCacheService = PlaceholderImageCacheService()
|
let placeholderImageCacheService = PlaceholderImageCacheService()
|
||||||
|
@ -69,7 +70,10 @@ class AppContext: ObservableObject {
|
||||||
emojiService = EmojiService(
|
emojiService = EmojiService(
|
||||||
apiService: apiService
|
apiService: apiService
|
||||||
)
|
)
|
||||||
|
|
||||||
statusPrefetchingService = StatusPrefetchingService(
|
statusPrefetchingService = StatusPrefetchingService(
|
||||||
|
managedObjectContext: _managedObjectContext,
|
||||||
|
backgroundManagedObjectContext: _backgroundManagedObjectContext,
|
||||||
apiService: _apiService
|
apiService: _apiService
|
||||||
)
|
)
|
||||||
let _notificationService = NotificationService(
|
let _notificationService = NotificationService(
|
||||||
|
@ -89,6 +93,11 @@ class AppContext: ObservableObject {
|
||||||
authenticationService: _authenticationService
|
authenticationService: _authenticationService
|
||||||
)
|
)
|
||||||
|
|
||||||
|
statusFilterService = StatusFilterService(
|
||||||
|
apiService: _apiService,
|
||||||
|
authenticationService: _authenticationService
|
||||||
|
)
|
||||||
|
|
||||||
documentStore = DocumentStore()
|
documentStore = DocumentStore()
|
||||||
documentStoreSubscription = documentStore.objectWillChange
|
documentStoreSubscription = documentStore.objectWillChange
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
|
|
@ -81,6 +81,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// reset notification badge
|
// reset notification badge
|
||||||
UserDefaults.shared.notificationBadgeCount = 0
|
UserDefaults.shared.notificationBadgeCount = 0
|
||||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||||
|
|
||||||
|
// trigger status filter update
|
||||||
|
AppContext.shared.statusFilterService.filterUpdatePublisher.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneWillResignActive(_ scene: UIScene) {
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
|
|
Loading…
Reference in New Issue