feat: add Community tab into discovery scene
This commit is contained in:
parent
dff4a5f118
commit
0548aa5f56
|
@ -233,6 +233,12 @@
|
||||||
DB3E6FF52807C40300B035AE /* DiscoveryForYouViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF42807C40300B035AE /* DiscoveryForYouViewController.swift */; };
|
DB3E6FF52807C40300B035AE /* DiscoveryForYouViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF42807C40300B035AE /* DiscoveryForYouViewController.swift */; };
|
||||||
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF72807C45300B035AE /* DiscoveryForYouViewModel.swift */; };
|
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF72807C45300B035AE /* DiscoveryForYouViewModel.swift */; };
|
||||||
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF92807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift */; };
|
DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FF92807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift */; };
|
||||||
|
DB3EA8E6281B79E200598866 /* DiscoveryCommunityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8E5281B79E200598866 /* DiscoveryCommunityViewController.swift */; };
|
||||||
|
DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8E8281B7A3700598866 /* DiscoveryCommunityViewModel.swift */; };
|
||||||
|
DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8EA281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift */; };
|
||||||
|
DB3EA8ED281B810100598866 /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8EC281B810100598866 /* APIService+PublicTimeline.swift */; };
|
||||||
|
DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8EE281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift */; };
|
||||||
|
DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3EA8F0281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift */; };
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; };
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; };
|
||||||
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; };
|
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; };
|
||||||
|
@ -962,6 +968,12 @@
|
||||||
DB3E6FF42807C40300B035AE /* DiscoveryForYouViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryForYouViewController.swift; sourceTree = "<group>"; };
|
DB3E6FF42807C40300B035AE /* DiscoveryForYouViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryForYouViewController.swift; sourceTree = "<group>"; };
|
||||||
DB3E6FF72807C45300B035AE /* DiscoveryForYouViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryForYouViewModel.swift; sourceTree = "<group>"; };
|
DB3E6FF72807C45300B035AE /* DiscoveryForYouViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryForYouViewModel.swift; sourceTree = "<group>"; };
|
||||||
DB3E6FF92807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryForYouViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
DB3E6FF92807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryForYouViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
|
DB3EA8E5281B79E200598866 /* DiscoveryCommunityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryCommunityViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DB3EA8E8281B7A3700598866 /* DiscoveryCommunityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryCommunityViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DB3EA8EA281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryCommunityViewModel+State.swift"; sourceTree = "<group>"; };
|
||||||
|
DB3EA8EC281B810100598866 /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||||
|
DB3EA8EE281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryCommunityViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
|
||||||
|
DB3EA8F0281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoveryCommunityViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
DB427DD525BAA00100D1B89D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
DB427DD525BAA00100D1B89D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2165,6 +2177,18 @@
|
||||||
path = ForYou;
|
path = ForYou;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB3EA8E7281B79E500598866 /* Community */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB3EA8E5281B79E200598866 /* DiscoveryCommunityViewController.swift */,
|
||||||
|
DB3EA8EE281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift */,
|
||||||
|
DB3EA8E8281B7A3700598866 /* DiscoveryCommunityViewModel.swift */,
|
||||||
|
DB3EA8F0281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift */,
|
||||||
|
DB3EA8EA281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift */,
|
||||||
|
);
|
||||||
|
path = Community;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB427DC925BAA00100D1B89D = {
|
DB427DC925BAA00100D1B89D = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2258,6 +2282,7 @@
|
||||||
2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */,
|
2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */,
|
||||||
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */,
|
||||||
DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */,
|
DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */,
|
||||||
|
DB3EA8EC281B810100598866 /* APIService+PublicTimeline.swift */,
|
||||||
DBA465922696B495002B41DB /* APIService+WebFinger.swift */,
|
DBA465922696B495002B41DB /* APIService+WebFinger.swift */,
|
||||||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */,
|
||||||
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */,
|
||||||
|
@ -3128,6 +3153,7 @@
|
||||||
DBDFF19828055A0900557A48 /* Posts */,
|
DBDFF19828055A0900557A48 /* Posts */,
|
||||||
DB3E6FDE2806A41200B035AE /* Hashtags */,
|
DB3E6FDE2806A41200B035AE /* Hashtags */,
|
||||||
DB3E6FED2806D7FC00B035AE /* News */,
|
DB3E6FED2806D7FC00B035AE /* News */,
|
||||||
|
DB3EA8E7281B79E500598866 /* Community */,
|
||||||
DB3E6FF62807C40500B035AE /* ForYou */,
|
DB3E6FF62807C40500B035AE /* ForYou */,
|
||||||
DBDFF19928055A1400557A48 /* DiscoveryViewController.swift */,
|
DBDFF19928055A1400557A48 /* DiscoveryViewController.swift */,
|
||||||
DBDFF19B28055BD600557A48 /* DiscoveryViewModel.swift */,
|
DBDFF19B28055BD600557A48 /* DiscoveryViewModel.swift */,
|
||||||
|
@ -4049,6 +4075,7 @@
|
||||||
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */,
|
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */,
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
||||||
|
DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */,
|
||||||
DB0FCB7227952986006C02E2 /* NamingState.swift in Sources */,
|
DB0FCB7227952986006C02E2 /* NamingState.swift in Sources */,
|
||||||
DB73BF47271199CA00781945 /* Instance.swift in Sources */,
|
DB73BF47271199CA00781945 /* Instance.swift in Sources */,
|
||||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
|
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
|
||||||
|
@ -4181,6 +4208,7 @@
|
||||||
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */,
|
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */,
|
||||||
DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */,
|
DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */,
|
||||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||||
|
DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */,
|
||||||
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */,
|
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */,
|
||||||
DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */,
|
DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */,
|
||||||
DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */,
|
DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */,
|
||||||
|
@ -4222,10 +4250,12 @@
|
||||||
DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */,
|
DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */,
|
||||||
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
||||||
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
|
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
|
||||||
|
DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */,
|
||||||
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */,
|
DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */,
|
||||||
DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */,
|
DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */,
|
||||||
|
DB3EA8E6281B79E200598866 /* DiscoveryCommunityViewController.swift in Sources */,
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
||||||
DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */,
|
DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */,
|
||||||
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */,
|
||||||
|
@ -4276,6 +4306,7 @@
|
||||||
DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */,
|
DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */,
|
||||||
DB336F21278D6D960031E64B /* MastodonEmoji.swift in Sources */,
|
DB336F21278D6D960031E64B /* MastodonEmoji.swift in Sources */,
|
||||||
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
|
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
|
||||||
|
DB3EA8ED281B810100598866 /* APIService+PublicTimeline.swift in Sources */,
|
||||||
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
|
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
|
||||||
DB025B97278D66D5002F581E /* MastodonUser+Property.swift in Sources */,
|
DB025B97278D66D5002F581E /* MastodonUser+Property.swift in Sources */,
|
||||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||||
|
@ -4338,6 +4369,7 @@
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
|
||||||
DB63F756279949BD00455B82 /* Persistence+SearchHistory.swift in Sources */,
|
DB63F756279949BD00455B82 /* Persistence+SearchHistory.swift in Sources */,
|
||||||
2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */,
|
2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */,
|
||||||
|
DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */,
|
||||||
DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */,
|
DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */,
|
||||||
DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */,
|
DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */,
|
||||||
DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */,
|
DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */,
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
// Generated using Sourcery 1.6.1 — https://github.com/krzysztofzablocki/Sourcery
|
// Generated using Sourcery 1.6.1 — https://github.com/krzysztofzablocki/Sourcery
|
||||||
// DO NOT EDIT
|
// DO NOT EDIT
|
||||||
|
|
||||||
|
// sourcery:inline:DiscoveryCommunityViewController.AutoGenerateTableViewDelegate
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// sourcery:inline:UserTimelineViewController.AutoGenerateTableViewDelegate
|
|
||||||
|
|
||||||
// Generated using Sourcery
|
// Generated using Sourcery
|
||||||
// DO NOT EDIT
|
// DO NOT EDIT
|
||||||
|
@ -33,3 +26,13 @@ func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith con
|
||||||
}
|
}
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
//
|
||||||
|
// DiscoveryCommunityViewViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import GameplayKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class DiscoveryCommunityViewViewModel {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "DiscoveryCommunityViewViewModel", category: "ViewModel")
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
||||||
|
let statusFetchedResultsController: StatusFetchedResultsController
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
||||||
|
private(set) lazy var stateMachine: GKStateMachine = {
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
State.Initial(viewModel: self),
|
||||||
|
State.Reloading(viewModel: self),
|
||||||
|
State.Fail(viewModel: self),
|
||||||
|
State.Idle(viewModel: self),
|
||||||
|
State.Loading(viewModel: self),
|
||||||
|
State.NoMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(State.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
self.context = context
|
||||||
|
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
domain: nil,
|
||||||
|
additionalTweetPredicate: nil
|
||||||
|
)
|
||||||
|
// end init
|
||||||
|
|
||||||
|
context.authenticationService.activeMastodonAuthentication
|
||||||
|
.map { $0?.domain }
|
||||||
|
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
// DiscoveryCommunityViewController+DataSourceProvider.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension DiscoveryCommunityViewController: DataSourceProvider {
|
||||||
|
func item(from source: DataSourceItem.Source) async -> DataSourceItem? {
|
||||||
|
var _indexPath = source.indexPath
|
||||||
|
if _indexPath == nil, let cell = source.tableViewCell {
|
||||||
|
_indexPath = await self.indexPath(for: cell)
|
||||||
|
}
|
||||||
|
guard let indexPath = _indexPath else { return nil }
|
||||||
|
|
||||||
|
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .status(let record):
|
||||||
|
return .status(record: record)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func indexPath(for cell: UITableViewCell) async -> IndexPath? {
|
||||||
|
return tableView.indexPath(for: cell)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
//
|
||||||
|
// DiscoveryCommunityViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
|
// Local Timeline
|
||||||
|
final class DiscoveryCommunityViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "DiscoveryCommunityViewController", category: "ViewController")
|
||||||
|
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var viewModel: DiscoveryCommunityViewModel!
|
||||||
|
|
||||||
|
let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
||||||
|
|
||||||
|
lazy var tableView: UITableView = {
|
||||||
|
let tableView = UITableView()
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 100
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiscoveryCommunityViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||||
|
ThemeService.shared.currentTheme
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] theme in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.view.backgroundColor = theme.secondarySystemBackgroundColor
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(tableView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
refreshControl.addTarget(self, action: #selector(DiscoveryCommunityViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||||
|
viewModel.didLoadLatest
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
tableView.delegate = self
|
||||||
|
viewModel.setupDiffableDataSource(
|
||||||
|
tableView: tableView,
|
||||||
|
statusTableViewCellDelegate: self
|
||||||
|
)
|
||||||
|
|
||||||
|
// setup batch fetch
|
||||||
|
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
|
||||||
|
viewModel.listBatchFetchViewModel.shouldFetch
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard self.view.window != nil else { return }
|
||||||
|
self.viewModel.stateMachine.enter(DiscoveryCommunityViewModel.State.Loading.self)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
viewModel.viewDidAppeared.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiscoveryCommunityViewController {
|
||||||
|
|
||||||
|
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||||
|
if !viewModel.stateMachine.enter(DiscoveryCommunityViewModel.State.Reloading.self) {
|
||||||
|
refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension DiscoveryCommunityViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
|
||||||
|
// sourcery:inline:CommunityViewController.AutoGenerateTableViewDelegate
|
||||||
|
|
||||||
|
// Generated using Sourcery
|
||||||
|
// DO NOT EDIT
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||||
|
return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||||
|
return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
|
aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
|
||||||
|
}
|
||||||
|
// sourcery:end
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - StatusTableViewCellDelegate
|
||||||
|
extension DiscoveryCommunityViewController: StatusTableViewCellDelegate { }
|
||||||
|
|
||||||
|
// MARK: ScrollViewContainer
|
||||||
|
extension DiscoveryCommunityViewController: ScrollViewContainer {
|
||||||
|
var scrollView: UIScrollView? {
|
||||||
|
tableView
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
//
|
||||||
|
// DiscoveryCommunityViewModel+Diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension DiscoveryCommunityViewModel {
|
||||||
|
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
tableView: UITableView,
|
||||||
|
statusTableViewCellDelegate: StatusTableViewCellDelegate
|
||||||
|
) {
|
||||||
|
diffableDataSource = StatusSection.diffableDataSource(
|
||||||
|
tableView: tableView,
|
||||||
|
context: context,
|
||||||
|
configuration: StatusSection.Configuration(
|
||||||
|
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: nil,
|
||||||
|
filterContext: .none,
|
||||||
|
activeFilters: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
stateMachine.enter(State.Reloading.self)
|
||||||
|
|
||||||
|
statusFetchedResultsController.$records
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] records in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<StatusSection, StatusItem>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
|
||||||
|
let items = records.map { StatusItem.status(record: $0) }
|
||||||
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
|
||||||
|
if let currentState = self.stateMachine.currentState {
|
||||||
|
switch currentState {
|
||||||
|
case is State.Initial,
|
||||||
|
is State.Reloading,
|
||||||
|
is State.Loading,
|
||||||
|
is State.Idle,
|
||||||
|
is State.Fail:
|
||||||
|
if !items.isEmpty {
|
||||||
|
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||||
|
}
|
||||||
|
case is State.NoMore:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diffableDataSource.applySnapshot(snapshot, animated: false)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
//
|
||||||
|
// DiscoveryCommunityViewModel+State.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import GameplayKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension DiscoveryCommunityViewModel {
|
||||||
|
class State: GKState, NamingState {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "DiscoveryCommunityViewModel.State", category: "StateMachine")
|
||||||
|
|
||||||
|
let id = UUID()
|
||||||
|
|
||||||
|
var name: String {
|
||||||
|
String(describing: Self.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
weak var viewModel: DiscoveryCommunityViewModel?
|
||||||
|
|
||||||
|
init(viewModel: DiscoveryCommunityViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
let previousState = previousState as? DiscoveryCommunityViewModel.State
|
||||||
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "<nil>")")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func enter(state: State.Type) {
|
||||||
|
stateMachine?.enter(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiscoveryCommunityViewModel.State {
|
||||||
|
class Initial: DiscoveryCommunityViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reloading: DiscoveryCommunityViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let _ = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
stateMachine.enter(Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fail: DiscoveryCommunityViewModel.State {
|
||||||
|
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let _ = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading 3s later…", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
stateMachine.enter(Loading.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Idle: DiscoveryCommunityViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type, is Loading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Loading: DiscoveryCommunityViewModel.State {
|
||||||
|
|
||||||
|
var maxID: String?
|
||||||
|
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Fail.Type:
|
||||||
|
return true
|
||||||
|
case is Idle.Type:
|
||||||
|
return true
|
||||||
|
case is NoMore.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||||
|
|
||||||
|
switch previousState {
|
||||||
|
case is Reloading:
|
||||||
|
maxID = nil
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
stateMachine.enter(Fail.self)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxID = self.maxID
|
||||||
|
let isReloading = maxID == nil
|
||||||
|
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let response = try await viewModel.context.apiService.publicTimeline(
|
||||||
|
query: .init(
|
||||||
|
local: true,
|
||||||
|
remote: nil,
|
||||||
|
onlyMedia: nil,
|
||||||
|
maxID: maxID,
|
||||||
|
sinceID: nil,
|
||||||
|
minID: nil,
|
||||||
|
limit: 20
|
||||||
|
),
|
||||||
|
authenticationBox: authenticationBox
|
||||||
|
)
|
||||||
|
|
||||||
|
let newMaxID = response.link?.maxID
|
||||||
|
let hasMore = newMaxID != nil
|
||||||
|
self.maxID = newMaxID
|
||||||
|
|
||||||
|
var hasNewStatusesAppend = false
|
||||||
|
var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs.value
|
||||||
|
for status in response.value {
|
||||||
|
guard !statusIDs.contains(status.id) else { continue }
|
||||||
|
statusIDs.append(status.id)
|
||||||
|
hasNewStatusesAppend = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasNewStatusesAppend, hasMore {
|
||||||
|
self.maxID = response.link?.maxID
|
||||||
|
await enter(state: Idle.self)
|
||||||
|
} else {
|
||||||
|
await enter(state: NoMore.self)
|
||||||
|
}
|
||||||
|
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||||
|
viewModel.didLoadLatest.send()
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user timeline fail: \(error.localizedDescription)")
|
||||||
|
await enter(state: Fail.self)
|
||||||
|
}
|
||||||
|
} // end Task
|
||||||
|
} // end func
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoMore: DiscoveryCommunityViewModel.State {
|
||||||
|
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||||
|
switch stateClass {
|
||||||
|
case is Reloading.Type:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnter(from previousState: GKState?) {
|
||||||
|
super.didEnter(from: previousState)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
//
|
||||||
|
// DiscoveryCommunityViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import GameplayKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class DiscoveryCommunityViewModel {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "DiscoveryCommunityViewModel", category: "ViewModel")
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
||||||
|
let statusFetchedResultsController: StatusFetchedResultsController
|
||||||
|
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
||||||
|
private(set) lazy var stateMachine: GKStateMachine = {
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
State.Initial(viewModel: self),
|
||||||
|
State.Reloading(viewModel: self),
|
||||||
|
State.Fail(viewModel: self),
|
||||||
|
State.Idle(viewModel: self),
|
||||||
|
State.Loading(viewModel: self),
|
||||||
|
State.NoMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(State.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
self.context = context
|
||||||
|
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
domain: nil,
|
||||||
|
additionalTweetPredicate: nil
|
||||||
|
)
|
||||||
|
// end init
|
||||||
|
|
||||||
|
context.authenticationService.activeMastodonAuthentication
|
||||||
|
.map { $0?.domain }
|
||||||
|
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
//
|
||||||
|
// DiscoveryCommunityViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import GameplayKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
final class DiscoveryCommunityViewModel {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "DiscoveryCommunityViewModel", category: "ViewModel")
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let viewDidAppeared = PassthroughSubject<Void, Never>()
|
||||||
|
let statusFetchedResultsController: StatusFetchedResultsController
|
||||||
|
|
||||||
|
// output
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
||||||
|
private(set) lazy var stateMachine: GKStateMachine = {
|
||||||
|
let stateMachine = GKStateMachine(states: [
|
||||||
|
State.Initial(viewModel: self),
|
||||||
|
State.Reloading(viewModel: self),
|
||||||
|
State.Fail(viewModel: self),
|
||||||
|
State.Idle(viewModel: self),
|
||||||
|
State.Loading(viewModel: self),
|
||||||
|
State.NoMore(viewModel: self),
|
||||||
|
])
|
||||||
|
stateMachine.enter(State.Initial.self)
|
||||||
|
return stateMachine
|
||||||
|
}()
|
||||||
|
|
||||||
|
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
init(context: AppContext) {
|
||||||
|
self.context = context
|
||||||
|
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
domain: nil,
|
||||||
|
additionalTweetPredicate: nil
|
||||||
|
)
|
||||||
|
// end init
|
||||||
|
|
||||||
|
context.authenticationService.activeMastodonAuthentication
|
||||||
|
.map { $0?.domain }
|
||||||
|
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Tabman
|
import Tabman
|
||||||
import Pageboy
|
import Pageboy
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
final class DiscoveryViewModel {
|
final class DiscoveryViewModel {
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ final class DiscoveryViewModel {
|
||||||
let discoveryPostsViewController: DiscoveryPostsViewController
|
let discoveryPostsViewController: DiscoveryPostsViewController
|
||||||
let discoveryHashtagsViewController: DiscoveryHashtagsViewController
|
let discoveryHashtagsViewController: DiscoveryHashtagsViewController
|
||||||
let discoveryNewsViewController: DiscoveryNewsViewController
|
let discoveryNewsViewController: DiscoveryNewsViewController
|
||||||
|
let discoveryCommunityViewController: DiscoveryCommunityViewController
|
||||||
let discoveryForYouViewController: DiscoveryForYouViewController
|
let discoveryForYouViewController: DiscoveryForYouViewController
|
||||||
|
|
||||||
@Published var viewControllers: [ScrollViewContainer & PageViewController]
|
@Published var viewControllers: [ScrollViewContainer & PageViewController]
|
||||||
|
@ -48,6 +50,12 @@ final class DiscoveryViewModel {
|
||||||
viewController.viewModel = DiscoveryNewsViewModel(context: context)
|
viewController.viewModel = DiscoveryNewsViewModel(context: context)
|
||||||
return viewController
|
return viewController
|
||||||
}()
|
}()
|
||||||
|
discoveryCommunityViewController = {
|
||||||
|
let viewController = DiscoveryCommunityViewController()
|
||||||
|
setupDependency(viewController)
|
||||||
|
viewController.viewModel = DiscoveryCommunityViewModel(context: context)
|
||||||
|
return viewController
|
||||||
|
}()
|
||||||
discoveryForYouViewController = {
|
discoveryForYouViewController = {
|
||||||
let viewController = DiscoveryForYouViewController()
|
let viewController = DiscoveryForYouViewController()
|
||||||
setupDependency(viewController)
|
setupDependency(viewController)
|
||||||
|
@ -58,6 +66,7 @@ final class DiscoveryViewModel {
|
||||||
discoveryPostsViewController,
|
discoveryPostsViewController,
|
||||||
discoveryHashtagsViewController,
|
discoveryHashtagsViewController,
|
||||||
discoveryNewsViewController,
|
discoveryNewsViewController,
|
||||||
|
discoveryCommunityViewController,
|
||||||
discoveryForYouViewController,
|
discoveryForYouViewController,
|
||||||
]
|
]
|
||||||
// end init
|
// end init
|
||||||
|
@ -123,7 +132,7 @@ protocol PageViewController: UIViewController {
|
||||||
|
|
||||||
// MARK: - PageViewController
|
// MARK: - PageViewController
|
||||||
extension DiscoveryPostsViewController: PageViewController {
|
extension DiscoveryPostsViewController: PageViewController {
|
||||||
var tabItemTitle: String { "Posts" }
|
var tabItemTitle: String { L10n.Scene.Discovery.Tabs.posts }
|
||||||
var tabItem: TMBarItemable {
|
var tabItem: TMBarItemable {
|
||||||
return TMBarItem(title: tabItemTitle)
|
return TMBarItem(title: tabItemTitle)
|
||||||
}
|
}
|
||||||
|
@ -132,7 +141,7 @@ extension DiscoveryPostsViewController: PageViewController {
|
||||||
|
|
||||||
// MARK: - PageViewController
|
// MARK: - PageViewController
|
||||||
extension DiscoveryHashtagsViewController: PageViewController {
|
extension DiscoveryHashtagsViewController: PageViewController {
|
||||||
var tabItemTitle: String { "Hashtags" }
|
var tabItemTitle: String { L10n.Scene.Discovery.Tabs.hashtags }
|
||||||
var tabItem: TMBarItemable {
|
var tabItem: TMBarItemable {
|
||||||
|
|
||||||
return TMBarItem(title: tabItemTitle)
|
return TMBarItem(title: tabItemTitle)
|
||||||
|
@ -141,7 +150,15 @@ extension DiscoveryHashtagsViewController: PageViewController {
|
||||||
|
|
||||||
// MARK: - PageViewController
|
// MARK: - PageViewController
|
||||||
extension DiscoveryNewsViewController: PageViewController {
|
extension DiscoveryNewsViewController: PageViewController {
|
||||||
var tabItemTitle: String { "News" }
|
var tabItemTitle: String { L10n.Scene.Discovery.Tabs.news }
|
||||||
|
var tabItem: TMBarItemable {
|
||||||
|
return TMBarItem(title: tabItemTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - PageViewController
|
||||||
|
extension DiscoveryCommunityViewController: PageViewController {
|
||||||
|
var tabItemTitle: String { "Community" }
|
||||||
var tabItem: TMBarItemable {
|
var tabItem: TMBarItemable {
|
||||||
return TMBarItem(title: tabItemTitle)
|
return TMBarItem(title: tabItemTitle)
|
||||||
}
|
}
|
||||||
|
@ -149,7 +166,7 @@ extension DiscoveryNewsViewController: PageViewController {
|
||||||
|
|
||||||
// MARK: - PageViewController
|
// MARK: - PageViewController
|
||||||
extension DiscoveryForYouViewController: PageViewController {
|
extension DiscoveryForYouViewController: PageViewController {
|
||||||
var tabItemTitle: String { "For You" }
|
var tabItemTitle: String { L10n.Scene.Discovery.Tabs.forYou }
|
||||||
var tabItem: TMBarItemable {
|
var tabItem: TMBarItemable {
|
||||||
return TMBarItem(title: tabItemTitle)
|
return TMBarItem(title: tabItemTitle)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,4 +73,5 @@ extension DiscoveryHashtagsViewModel {
|
||||||
hashtags = response.value.filter { !$0.name.isEmpty }
|
hashtags = response.value.filter { !$0.name.isEmpty }
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch tags: \(response.value.count)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch tags: \(response.value.count)")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,7 @@ extension DiscoveryPostsViewModel.State {
|
||||||
}
|
}
|
||||||
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||||
viewModel.didLoadLatest.send()
|
viewModel.didLoadLatest.send()
|
||||||
// } catch let error as?
|
|
||||||
} catch {
|
} catch {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch posts fail: \(error.localizedDescription)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch posts fail: \(error.localizedDescription)")
|
||||||
if let error = error as? Mastodon.API.Error, error.httpResponseStatus.code == 404 {
|
if let error = error as? Mastodon.API.Error, error.httpResponseStatus.code == 404 {
|
||||||
|
|
|
@ -195,6 +195,12 @@ extension UserTimelineViewModel.State {
|
||||||
|
|
||||||
// trigger data source update. otherwise, spinner always display
|
// trigger data source update. otherwise, spinner always display
|
||||||
viewModel.isSuspended.value = viewModel.isSuspended.value
|
viewModel.isSuspended.value = viewModel.isSuspended.value
|
||||||
|
|
||||||
|
// remove bottom loader
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||||
|
var snapshot = diffableDataSource.snapshot()
|
||||||
|
snapshot.deleteItems([.bottomLoader])
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
//
|
||||||
|
// APIService+PublicTimeline.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-4-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import CommonOSLog
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
func publicTimeline(
|
||||||
|
query: Mastodon.API.Timeline.PublicTimelineQuery,
|
||||||
|
authenticationBox: MastodonAuthenticationBox
|
||||||
|
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Status]> {
|
||||||
|
let domain = authenticationBox.domain
|
||||||
|
let authorization = authenticationBox.userAuthorization
|
||||||
|
|
||||||
|
let response = try await Mastodon.API.Timeline.public(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
).singleOutput()
|
||||||
|
|
||||||
|
let managedObjectContext = self.backgroundManagedObjectContext
|
||||||
|
try await managedObjectContext.performChanges {
|
||||||
|
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
||||||
|
for entity in response.value {
|
||||||
|
_ = Persistence.Status.createOrMerge(
|
||||||
|
in: managedObjectContext,
|
||||||
|
context: Persistence.Status.PersistContext(
|
||||||
|
domain: domain,
|
||||||
|
entity: entity,
|
||||||
|
me: me,
|
||||||
|
statusCache: nil,
|
||||||
|
userCache: nil,
|
||||||
|
networkDate: response.networkDate
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
} // end func
|
||||||
|
|
||||||
|
}
|
|
@ -48,7 +48,7 @@ extension APIService {
|
||||||
try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user
|
||||||
for entity in response.value {
|
for entity in response.value {
|
||||||
Persistence.Status.createOrMerge(
|
_ = Persistence.Status.createOrMerge(
|
||||||
in: managedObjectContext,
|
in: managedObjectContext,
|
||||||
context: Persistence.Status.PersistContext(
|
context: Persistence.Status.PersistContext(
|
||||||
domain: domain,
|
domain: domain,
|
||||||
|
|
|
@ -33,16 +33,18 @@ extension Mastodon.API.Timeline {
|
||||||
/// - session: `URLSession`
|
/// - session: `URLSession`
|
||||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
/// - query: `PublicTimelineQuery` with query parameters
|
/// - query: `PublicTimelineQuery` with query parameters
|
||||||
|
/// - authorization: required if the instance has disabled public preview
|
||||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||||
public static func `public`(
|
public static func `public`(
|
||||||
session: URLSession,
|
session: URLSession,
|
||||||
domain: String,
|
domain: String,
|
||||||
query: PublicTimelineQuery
|
query: PublicTimelineQuery,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization?
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> {
|
||||||
let request = Mastodon.API.get(
|
let request = Mastodon.API.get(
|
||||||
url: publicTimelineEndpointURL(domain: domain),
|
url: publicTimelineEndpointURL(domain: domain),
|
||||||
query: query,
|
query: query,
|
||||||
authorization: nil
|
authorization: authorization
|
||||||
)
|
)
|
||||||
return session.dataTaskPublisher(for: request)
|
return session.dataTaskPublisher(for: request)
|
||||||
.tryMap { data, response in
|
.tryMap { data, response in
|
||||||
|
|
Loading…
Reference in New Issue