chore: [WIP] inject AuthContext into ViewModel

This commit is contained in:
CMK 2022-10-09 20:07:57 +08:00
parent f73241caee
commit bb5c999bea
160 changed files with 1329 additions and 1435 deletions

View File

@ -7,6 +7,5 @@ set -eo pipefail
xcodebuild -workspace Mastodon.xcworkspace \
-scheme Mastodon \
-destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \
clean \
build | xcpretty

View File

@ -4109,7 +4109,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.7;
MARKETING_VERSION = 1.4.5;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -4139,7 +4139,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.7;
MARKETING_VERSION = 1.4.5;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -4312,7 +4312,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.7;
MARKETING_VERSION = 1.4.5;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -4609,7 +4609,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.7;
MARKETING_VERSION = 1.4.5;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -112,12 +112,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>7</integer>
<integer>25</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>6</integer>
<integer>24</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -1,241 +1,239 @@
{
"object": {
"pins" : [
{
"package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"branch": null,
"revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8",
"version" : "5.6.1"
}
},
{
"package": "AlamofireImage",
"repositoryURL": "https://github.com/Alamofire/AlamofireImage.git",
"identity" : "alamofireimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/AlamofireImage.git",
"state" : {
"branch": null,
"revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10",
"version" : "4.2.0"
}
},
{
"package": "CommonOSLog",
"repositoryURL": "https://github.com/MainasuK/CommonOSLog",
"identity" : "commonoslog",
"kind" : "remoteSourceControl",
"location" : "https://github.com/MainasuK/CommonOSLog",
"state" : {
"branch": null,
"revision" : "c121624a30698e9886efe38aebb36ff51c01b6c2",
"version" : "0.1.1"
}
},
{
"package": "FaviconFinder",
"repositoryURL": "https://github.com/will-lumley/FaviconFinder.git",
"identity" : "faviconfinder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/will-lumley/FaviconFinder.git",
"state" : {
"branch": null,
"revision" : "1f74844f77f79b95c0bb0130b3a87d4f340e6d3a",
"version" : "3.3.0"
}
},
{
"package": "FLAnimatedImage",
"repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git",
"identity" : "flanimatedimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Flipboard/FLAnimatedImage.git",
"state" : {
"branch": null,
"revision" : "e7f9fd4681ae41bf6f3056db08af4f401d61da52",
"version" : "1.0.16"
}
},
{
"package": "FPSIndicator",
"repositoryURL": "https://github.com/MainasuK/FPSIndicator.git",
"identity" : "fpsindicator",
"kind" : "remoteSourceControl",
"location" : "https://github.com/MainasuK/FPSIndicator.git",
"state" : {
"branch": null,
"revision" : "e4a5067ccd5293b024c767f09e51056afd4a4796",
"version" : "1.1.0"
}
},
{
"package": "Fuzi",
"repositoryURL": "https://github.com/cezheng/Fuzi.git",
"identity" : "fuzi",
"kind" : "remoteSourceControl",
"location" : "https://github.com/cezheng/Fuzi.git",
"state" : {
"branch": null,
"revision" : "f08c8323da21e985f3772610753bcfc652c2103f",
"version" : "3.1.3"
}
},
{
"package": "KeychainAccess",
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess.git",
"state" : {
"branch": null,
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"package": "MetaTextKit",
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
"identity" : "metatextkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TwidereProject/MetaTextKit.git",
"state" : {
"branch": null,
"revision" : "dcd5255d6930c2fab408dc8562c577547e477624",
"version" : "2.2.5"
}
},
{
"package": "Nuke",
"repositoryURL": "https://github.com/kean/Nuke.git",
"identity" : "nuke",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke.git",
"state" : {
"branch": null,
"revision" : "0ea7545b5c918285aacc044dc75048625c8257cc",
"version" : "10.8.0"
}
},
{
"package": "NukeFLAnimatedImagePlugin",
"repositoryURL": "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
"identity" : "nuke-flanimatedimage-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git",
"state" : {
"branch": null,
"revision" : "b59c346a7d536336db3b0f12c72c6e53ee709e16",
"version" : "8.0.0"
}
},
{
"package": "Pageboy",
"repositoryURL": "https://github.com/uias/Pageboy",
"identity" : "pageboy",
"kind" : "remoteSourceControl",
"location" : "https://github.com/uias/Pageboy",
"state" : {
"branch": null,
"revision" : "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6",
"version" : "3.6.2"
}
},
{
"package": "PanModal",
"repositoryURL": "https://github.com/slackhq/PanModal.git",
"identity" : "panmodal",
"kind" : "remoteSourceControl",
"location" : "https://github.com/slackhq/PanModal.git",
"state" : {
"branch": null,
"revision" : "b012aecb6b67a8e46369227f893c12544846613f",
"version" : "1.2.7"
}
},
{
"package": "SDWebImage",
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage.git",
"state" : {
"branch": null,
"revision" : "2e63d0061da449ad0ed130768d05dceb1496de44",
"version" : "5.12.5"
}
},
{
"package": "swift-collections",
"repositoryURL": "https://github.com/apple/swift-collections.git",
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"branch": null,
"revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
"version" : "1.0.3"
}
},
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"branch": null,
"revision" : "546610d52b19be3e19935e0880bb06b9c03f5cef",
"version" : "1.14.4"
}
},
{
"package": "swift-nio-zlib-support",
"repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
"identity" : "swift-nio-zlib-support",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-zlib-support.git",
"state" : {
"branch": null,
"revision" : "37760e9a52030bb9011972c5213c3350fa9d41fd",
"version" : "1.0.0"
}
},
{
"package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"branch": null,
"revision" : "41e7c263fb8c277e980ebcb9b0b5f6031d3d4886",
"version" : "2.4.2"
}
},
{
"package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
"state" : {
"branch": null,
"revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
"version" : "0.1.4"
}
},
{
"package": "SwiftyJSON",
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git",
"identity" : "swiftyjson",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
"state" : {
"branch": null,
"revision" : "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
"version" : "5.0.1"
}
},
{
"package": "TabBarPager",
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
"identity" : "tabbarpager",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TwidereProject/TabBarPager.git",
"state" : {
"branch": null,
"revision" : "488aa66d157a648901b61721212c0dec23d27ee5",
"version" : "0.1.0"
}
},
{
"package": "Tabman",
"repositoryURL": "https://github.com/uias/Tabman",
"identity" : "tabman",
"kind" : "remoteSourceControl",
"location" : "https://github.com/uias/Tabman",
"state" : {
"branch": null,
"revision" : "4a4f7c755b875ffd4f9ef10d67a67883669d2465",
"version" : "2.13.0"
}
},
{
"package": "ThirdPartyMailer",
"repositoryURL": "https://github.com/vtourraine/ThirdPartyMailer.git",
"identity" : "thirdpartymailer",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vtourraine/ThirdPartyMailer.git",
"state" : {
"branch": null,
"revision" : "44c1cfaa6969963f22691aa67f88a69e3b6d651f",
"version" : "2.1.0"
}
},
{
"package": "TOCropViewController",
"repositoryURL": "https://github.com/TimOliver/TOCropViewController.git",
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController.git",
"state" : {
"branch": null,
"revision" : "d0470491f56e734731bbf77991944c0dfdee3e0e",
"version" : "2.6.1"
}
},
{
"package": "UIHostingConfigurationBackport",
"repositoryURL": "https://github.com/woxtu/UIHostingConfigurationBackport.git",
"identity" : "uihostingconfigurationbackport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/woxtu/UIHostingConfigurationBackport.git",
"state" : {
"branch": null,
"revision" : "6091f2d38faa4b24fc2ca0389c651e2f666624a3",
"version" : "0.1.0"
}
},
{
"package": "UITextView+Placeholder",
"repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git",
"identity" : "uitextview-placeholder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/MainasuK/UITextView-Placeholder.git",
"state" : {
"branch": null,
"revision" : "20f513ded04a040cdf5467f0891849b1763ede3b",
"version" : "1.4.1"
}
}
]
},
"version": 1
],
"version" : 2
}

View File

@ -22,7 +22,7 @@ final public class SceneCoordinator {
private weak var sceneDelegate: SceneDelegate!
private weak var appContext: AppContext!
private var authContext: AuthContext?
private(set) var authContext: AuthContext?
let id = UUID().uuidString
@ -45,17 +45,14 @@ final public class SceneCoordinator {
appContext.notificationService.requestRevealNotificationPublisher
.receive(on: DispatchQueue.main)
.compactMap { [weak self] pushNotification -> AnyPublisher<MastodonPushNotification?, Never> in
guard let self = self else { return Just(nil).eraseToAnyPublisher() }
// skip if no available account
guard let currentActiveAuthenticationBox = appContext.authenticationService.activeMastodonAuthenticationBox.value else {
return Just(nil).eraseToAnyPublisher()
}
.sink(receiveValue: { [weak self] pushNotification in
guard let self = self else { return }
Task {
guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return }
let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
// do nothing if notification for current account
return Just(pushNotification).eraseToAnyPublisher()
return
} else {
// switch to notification's account
let request = MastodonAuthentication.sortedFetchRequest
@ -64,41 +61,19 @@ final public class SceneCoordinator {
request.fetchLimit = 1
do {
guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
return Just(nil).eraseToAnyPublisher()
return
}
let domain = authentication.domain
let userID = authentication.userID
return appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID)
.receive(on: DispatchQueue.main)
.map { [weak self] result -> MastodonPushNotification? in
guard let self = self else { return nil }
switch result {
case .success:
// reset view hierarchy
let isSuccess = try await appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID)
guard isSuccess else { return }
self.setup()
return pushNotification
case .failure:
return nil
}
}
.delay(for: 1, scheduler: DispatchQueue.main) // set delay to slow transition (not must)
.eraseToAnyPublisher()
} catch {
assertionFailure(error.localizedDescription)
return Just(nil).eraseToAnyPublisher()
}
}
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak self] pushNotification in
guard let self = self else { return }
guard let pushNotification = pushNotification else { return }
try await Task.sleep(nanoseconds: .second * 1)
// redirect to notification tab
self.switchToTabBar(tab: .notification)
// Delay in next run loop
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
@ -121,24 +96,32 @@ final public class SceneCoordinator {
// show notification related content
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
guard let authContext = self.authContext else { return }
let notificationID = String(pushNotification.notificationID)
switch type {
case .follow:
let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID)
self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
_ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
case .followRequest:
// do nothing
break
case .mention, .reblog, .favourite, .poll, .status:
let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID)
self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
_ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
case ._other:
assertionFailure()
break
}
} // end DispatchQueue.main.async
} catch {
assertionFailure(error.localizedDescription)
return
}
}
} // end Task
})
.store(in: &disposeBag)
}
}
@ -180,7 +163,7 @@ extension SceneCoordinator {
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
// profile
case accountList
case accountList(viewModel: AccountListViewModel)
case profile(viewModel: ProfileViewModel)
case favorite(viewModel: FavoriteViewModel)
case follower(viewModel: FollowerListViewModel)
@ -260,6 +243,19 @@ extension SceneCoordinator {
transition: .modal(animated: true, completion: nil)
)
}
} else {
let wizardViewController = WizardViewController()
if !wizardViewController.items.isEmpty,
let delegate = rootViewController as? WizardViewControllerDelegate
{
// do not add as child view controller.
// otherwise, the tab bar controller will add as a new tab
wizardViewController.delegate = delegate
wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
wizardViewController.view.frame = rootViewController.view.bounds
rootViewController.view.addSubview(wizardViewController.view)
self.wizardViewController = wizardViewController
}
}
} catch {
@ -431,8 +427,9 @@ private extension SceneCoordinator {
let _viewController = HashtagTimelineViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .accountList:
case .accountList(let viewModel):
let _viewController = AccountListViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .profile(let viewModel):
let _viewController = ProfileViewController()

View File

@ -10,4 +10,3 @@ import Foundation
enum ComposeStatusAttachmentSection: Hashable {
case main
}

View File

@ -23,13 +23,16 @@ extension DiscoverySection {
static let logger = Logger(subsystem: "DiscoverySection", category: "logic")
class Configuration {
let authContext: AuthContext
weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate?
let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher?
public init(
authContext: AuthContext,
profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil,
familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil
) {
self.authContext = authContext
self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate
self.familiarFollowers = familiarFollowers
}
@ -73,11 +76,9 @@ extension DiscoverySection {
} else {
cell.profileCardView.viewModel.familiarFollowers = nil
}
// bind me
cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
}
context.authenticationService.activeMastodonAuthentication
.map { $0?.user }
.assign(to: \.me, on: cell.profileCardView.viewModel.relationshipViewModel)
.store(in: &cell.disposeBag)
return cell
case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell

View File

@ -24,6 +24,7 @@ enum NotificationSection: Equatable, Hashable {
extension NotificationSection {
struct Configuration {
let authContext: AuthContext
weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate?
let filterContext: Mastodon.Entity.Filter.Context?
let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher?
@ -74,21 +75,20 @@ extension NotificationSection {
viewModel: NotificationTableViewCell.ViewModel,
configuration: Configuration
) {
cell.notificationView.viewModel.authContext = configuration.authContext
StatusSection.setupStatusPollDataSource(
context: context,
authContext: configuration.authContext,
statusView: cell.notificationView.statusView
)
StatusSection.setupStatusPollDataSource(
context: context,
authContext: configuration.authContext,
statusView: cell.notificationView.quoteStatusView
)
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.notificationView.viewModel)
.store(in: &cell.disposeBag)
cell.configure(
tableView: tableView,
viewModel: viewModel,

View File

@ -133,6 +133,7 @@ enum RecommendAccountSection: Equatable, Hashable {
extension RecommendAccountSection {
struct Configuration {
let authContext: AuthContext
weak var suggestionAccountTableViewCellDelegate: SuggestionAccountTableViewCellDelegate?
}
@ -150,10 +151,7 @@ extension RecommendAccountSection {
cell.configure(user: user)
}
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.viewModel)
.store(in: &cell.disposeBag)
cell.viewModel.userIdentifier = configuration.authContext.mastodonAuthenticationBox
cell.delegate = configuration.suggestionAccountTableViewCellDelegate
}
return cell

View File

@ -23,6 +23,7 @@ enum ReportSection: Equatable, Hashable {
extension ReportSection {
struct Configuration {
let authContext: AuthContext
}
static func diffableDataSource(
@ -101,13 +102,11 @@ extension ReportSection {
) {
StatusSection.setupStatusPollDataSource(
context: context,
authContext: configuration.authContext,
statusView: cell.statusView
)
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.statusView.viewModel.authContext = configuration.authContext
cell.configure(
tableView: tableView,

View File

@ -25,6 +25,7 @@ extension SearchResultSection {
static let logger = Logger(subsystem: "SearchResultSection", category: "logic")
struct Configuration {
let authContext: AuthContext
weak var statusViewTableViewCellDelegate: StatusTableViewCellDelegate?
weak var userTableViewCellDelegate: UserTableViewCellDelegate?
}
@ -99,13 +100,11 @@ extension SearchResultSection {
) {
StatusSection.setupStatusPollDataSource(
context: context,
authContext: configuration.authContext,
statusView: cell.statusView
)
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.statusView.viewModel.authContext = configuration.authContext
cell.configure(
tableView: tableView,

View File

@ -27,6 +27,7 @@ extension StatusSection {
static let logger = Logger(subsystem: "StatusSection", category: "logic")
struct Configuration {
let authContext: AuthContext
weak var statusTableViewCellDelegate: StatusTableViewCellDelegate?
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
let filterContext: Mastodon.Entity.Filter.Context?
@ -159,6 +160,7 @@ extension StatusSection {
public static func setupStatusPollDataSource(
context: AppContext,
authContext: AuthContext,
statusView: StatusView
) {
let managedObjectContext = context.managedObjectContext
@ -172,10 +174,7 @@ extension StatusSection {
return _cell ?? PollOptionTableViewCell()
}()
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.pollOptionView.viewModel)
.store(in: &cell.disposeBag)
cell.pollOptionView.viewModel.authContext = authContext
managedObjectContext.performAndWait {
guard let option = record.object(in: managedObjectContext) else {
@ -212,14 +211,13 @@ extension StatusSection {
return true
}()
if needsUpdatePoll, let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value
{
if needsUpdatePoll {
let pollRecord: ManagedObjectRecord<Poll> = .init(objectID: option.poll.objectID)
Task { [weak context] in
guard let context = context else { return }
_ = try await context.apiService.poll(
poll: pollRecord,
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
}
}
@ -248,13 +246,11 @@ extension StatusSection {
) {
setupStatusPollDataSource(
context: context,
authContext: configuration.authContext,
statusView: cell.statusView
)
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.statusView.viewModel.authContext = configuration.authContext
cell.configure(
tableView: tableView,
@ -277,13 +273,11 @@ extension StatusSection {
) {
setupStatusPollDataSource(
context: context,
authContext: configuration.authContext,
statusView: cell.statusView
)
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
.store(in: &cell.disposeBag)
cell.statusView.viewModel.authContext = configuration.authContext
cell.configure(
tableView: tableView,

View File

@ -11,16 +11,15 @@ import MastodonCore
extension DataSourceFacade {
static func responseToUserBlockAction(
dependency: NeedsDependency,
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleBlock(
user: user,
authenticationBox: authenticationBox
authenticationBox: dependency.authContext.mastodonAuthenticationBox
)
} // end func
}

View File

@ -12,16 +12,15 @@ import MastodonCore
extension DataSourceFacade {
public static func responseToStatusBookmarkAction(
provider: DataSourceProvider,
status: ManagedObjectRecord<Status>,
authenticationBox: MastodonAuthenticationBox
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await provider.context.apiService.bookmark(
record: status,
authenticationBox: authenticationBox
authenticationBox: provider.authContext.mastodonAuthenticationBox
)
}
}

View File

@ -12,16 +12,15 @@ import MastodonCore
extension DataSourceFacade {
public static func responseToStatusFavoriteAction(
provider: DataSourceProvider,
status: ManagedObjectRecord<Status>,
authenticationBox: MastodonAuthenticationBox
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await provider.context.apiService.favorite(
record: status,
authenticationBox: authenticationBox
authenticationBox: provider.authContext.mastodonAuthenticationBox
)
}
}

View File

@ -14,26 +14,24 @@ import MastodonLocalization
extension DataSourceFacade {
static func responseToUserFollowAction(
dependency: NeedsDependency,
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleFollow(
user: user,
authenticationBox: authenticationBox
authenticationBox: dependency.authContext.mastodonAuthenticationBox
)
} // end func
}
extension DataSourceFacade {
static func responseToUserFollowRequestAction(
dependency: NeedsDependency,
dependency: NeedsDependency & AuthContextProvider,
notification: ManagedObjectRecord<Notification>,
query: Mastodon.API.Account.FollowReqeustQuery,
authenticationBox: MastodonAuthenticationBox
query: Mastodon.API.Account.FollowReqeustQuery
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
@ -72,7 +70,7 @@ extension DataSourceFacade {
_ = try await dependency.context.apiService.followRequest(
userID: userID,
query: query,
authenticationBox: authenticationBox
authenticationBox: dependency.authContext.mastodonAuthenticationBox
)
} catch {
// reset state when failure

View File

@ -7,12 +7,13 @@
import UIKit
import CoreDataStack
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
@MainActor
static func coordinateToHashtagScene(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
tag: DataSourceItem.TagKind
) async {
switch tag {
@ -25,11 +26,12 @@ extension DataSourceFacade {
@MainActor
static func coordinateToHashtagScene(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
tag: Mastodon.Entity.Tag
) async {
let hashtagTimelineViewModel = HashtagTimelineViewModel(
context: provider.context,
authContext: provider.authContext,
hashtag: tag.name
)
@ -42,7 +44,7 @@ extension DataSourceFacade {
@MainActor
static func coordinateToHashtagScene(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
tag: ManagedObjectRecord<Tag>
) async {
let managedObjectContext = provider.context.managedObjectContext
@ -55,6 +57,7 @@ extension DataSourceFacade {
let hashtagTimelineViewModel = HashtagTimelineViewModel(
context: provider.context,
authContext: provider.authContext,
hashtag: name
)

View File

@ -8,11 +8,12 @@
import Foundation
import CoreDataStack
import MetaTextKit
import MastodonCore
extension DataSourceFacade {
static func responseToMetaTextAction(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
target: StatusTarget,
status: ManagedObjectRecord<Status>,
meta: Meta
@ -33,7 +34,7 @@ extension DataSourceFacade {
}
static func responseToMetaTextAction(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>,
meta: Meta
) async {
@ -47,19 +48,20 @@ extension DataSourceFacade {
assertionFailure()
return
}
if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain,
let domain = provider.authContext.mastodonAuthenticationBox.domain
if url.host == domain,
url.pathComponents.count >= 4,
url.pathComponents[0] == "/",
url.pathComponents[1] == "web",
url.pathComponents[2] == "statuses" {
let statusID = url.pathComponents[3]
let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID)
let threadViewModel = RemoteThreadViewModel(context: provider.context, authContext: provider.authContext, statusID: statusID)
await provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show)
} else {
await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
}
case .hashtag(_, let hashtag, _):
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: hashtag)
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag)
await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show)
case .mention(_, let mention, let userInfo):
await coordinateToProfileScene(

View File

@ -11,16 +11,15 @@ import MastodonCore
extension DataSourceFacade {
static func responseToUserMuteAction(
dependency: NeedsDependency,
user: ManagedObjectRecord<MastodonUser>,
authenticationBox: MastodonAuthenticationBox
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await dependency.context.apiService.toggleMute(
user: user,
authenticationBox: authenticationBox
authenticationBox: dependency.authContext.mastodonAuthenticationBox
)
} // end func
}

View File

@ -7,11 +7,12 @@
import UIKit
import CoreDataStack
import MastodonCore
extension DataSourceFacade {
static func coordinateToProfileScene(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
target: StatusTarget,
status: ManagedObjectRecord<Status>
) async {
@ -32,7 +33,7 @@ extension DataSourceFacade {
@MainActor
static func coordinateToProfileScene(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>
) async {
guard let user = user.object(in: provider.context.managedObjectContext) else {
@ -42,6 +43,7 @@ extension DataSourceFacade {
let profileViewModel = CachedProfileViewModel(
context: provider.context,
authContext: provider.authContext,
mastodonUser: user
)
@ -57,13 +59,12 @@ extension DataSourceFacade {
extension DataSourceFacade {
static func coordinateToProfileScene(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>,
mention: String, // username,
userInfo: [AnyHashable: Any]?
) async {
guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let domain = authenticationBox.domain
let domain = provider.authContext.mastodonAuthenticationBox.domain
let href = userInfo?["href"] as? String
guard let url = href.flatMap({ URL(string: $0) }) else { return }
@ -85,8 +86,8 @@ extension DataSourceFacade {
let userID = mention.id
let profileViewModel: ProfileViewModel = {
// check if self
guard userID != authenticationBox.userID else {
return MeProfileViewModel(context: provider.context)
guard userID != provider.authContext.mastodonAuthenticationBox.userID else {
return MeProfileViewModel(context: provider.context, authContext: provider.authContext)
}
let request = MastodonUser.sortedFetchRequest
@ -95,9 +96,9 @@ extension DataSourceFacade {
let _user = provider.context.managedObjectContext.safeFetch(request).first
if let user = _user {
return CachedProfileViewModel(context: provider.context, mastodonUser: user)
return CachedProfileViewModel(context: provider.context, authContext: provider.authContext, mastodonUser: user)
} else {
return RemoteProfileViewModel(context: provider.context, userID: userID)
return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID)
}
}()

View File

@ -12,16 +12,15 @@ import MastodonUI
extension DataSourceFacade {
static func responseToStatusReblogAction(
provider: DataSourceProvider,
status: ManagedObjectRecord<Status>,
authenticationBox: MastodonAuthenticationBox
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>
) async throws {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
_ = try await provider.context.apiService.reblog(
record: status,
authenticationBox: authenticationBox
authenticationBox: provider.authContext.mastodonAuthenticationBox
)
} // end func
}

View File

@ -12,18 +12,18 @@ import MastodonCore
extension DataSourceFacade {
static func responseToCreateSearchHistory(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
item: DataSourceItem
) async {
switch item {
case .status:
break // not create search history for status
case .user(let record):
let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value
let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext
try? await managedObjectContext.performChanges {
guard let me = authenticationBox?.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let user = record.object(in: managedObjectContext) else { return }
_ = Persistence.SearchHistory.createOrMerge(
in: managedObjectContext,
@ -35,13 +35,12 @@ extension DataSourceFacade {
)
} // end try? await managedObjectContext.performChanges { }
case .hashtag(let tag):
let _authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value
let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext
switch tag {
case .entity(let entity):
try? await managedObjectContext.performChanges {
guard let authenticationBox = _authenticationBox else { return }
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
let now = Date()
@ -67,7 +66,7 @@ extension DataSourceFacade {
} // end try? await managedObjectContext.performChanges { }
case .record(let record):
try? await managedObjectContext.performChanges {
guard let authenticationBox = _authenticationBox else { return }
let authenticationBox = provider.authContext.mastodonAuthenticationBox
guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
guard let tag = record.object(in: managedObjectContext) else { return }
@ -93,13 +92,12 @@ extension DataSourceFacade {
extension DataSourceFacade {
static func responseToDeleteSearchHistory(
provider: DataSourceProvider
provider: DataSourceProvider & AuthContextProvider
) async throws {
let _authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value
let authenticationBox = provider.authContext.mastodonAuthenticationBox
let managedObjectContext = provider.context.backgroundManagedObjectContext
try await managedObjectContext.performChanges {
guard let authenticationBox = _authenticationBox else { return }
guard let _ = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return }
let request = SearchHistory.sortedFetchRequest
request.predicate = SearchHistory.predicate(

View File

@ -15,13 +15,12 @@ import MastodonLocalization
extension DataSourceFacade {
static func responseToDeleteStatus(
dependency: NeedsDependency,
status: ManagedObjectRecord<Status>,
authenticationBox: MastodonAuthenticationBox
dependency: NeedsDependency & AuthContextProvider,
status: ManagedObjectRecord<Status>
) async throws {
_ = try await dependency.context.apiService.deleteStatus(
status: status,
authenticationBox: authenticationBox
authenticationBox: dependency.authContext.mastodonAuthenticationBox
)
}
@ -81,10 +80,9 @@ extension DataSourceFacade {
extension DataSourceFacade {
@MainActor
static func responseToActionToolbar(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
status: ManagedObjectRecord<Status>,
action: ActionToolbarContainer.Action,
authenticationBox: MastodonAuthenticationBox,
sender: UIButton
) async throws {
let managedObjectContext = provider.context.managedObjectContext
@ -100,16 +98,15 @@ extension DataSourceFacade {
switch action {
case .reply:
guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
selectionFeedbackGenerator.selectionChanged()
let composeViewModel = ComposeViewModel(
context: provider.context,
composeKind: .reply(status: status),
authenticationBox: authenticationBox
authContext: provider.authContext
)
provider.coordinator.present(
_ = provider.coordinator.present(
scene: .compose(viewModel: composeViewModel),
from: provider,
transition: .modal(animated: true, completion: nil)
@ -117,20 +114,17 @@ extension DataSourceFacade {
case .reblog:
try await DataSourceFacade.responseToStatusReblogAction(
provider: provider,
status: status,
authenticationBox: authenticationBox
status: status
)
case .like:
try await DataSourceFacade.responseToStatusFavoriteAction(
provider: provider,
status: status,
authenticationBox: authenticationBox
status: status
)
case .bookmark:
try await DataSourceFacade.responseToStatusBookmarkAction(
provider: provider,
status: status,
authenticationBox: authenticationBox
status: status
)
case .share:
try await DataSourceFacade.responseToStatusShareAction(
@ -155,10 +149,9 @@ extension DataSourceFacade {
@MainActor
static func responseToMenuAction(
dependency: NeedsDependency & UIViewController,
dependency: UIViewController & NeedsDependency & AuthContextProvider,
action: MastodonMenu.Action,
menuContext: MenuContext,
authenticationBox: MastodonAuthenticationBox
menuContext: MenuContext
) async throws {
switch action {
case .muteUser(let actionContext):
@ -181,8 +174,7 @@ extension DataSourceFacade {
guard let user = _user else { return }
try await DataSourceFacade.responseToUserMuteAction(
dependency: dependency,
user: user,
authenticationBox: authenticationBox
user: user
)
} // end Task
}
@ -210,8 +202,7 @@ extension DataSourceFacade {
guard let user = _user else { return }
try await DataSourceFacade.responseToUserBlockAction(
dependency: dependency,
user: user,
authenticationBox: authenticationBox
user: user
)
} // end Task
}
@ -225,11 +216,12 @@ extension DataSourceFacade {
let reportViewModel = ReportViewModel(
context: dependency.context,
authContext: dependency.authContext,
user: user,
status: menuContext.status
)
dependency.coordinator.present(
_ = dependency.coordinator.present(
scene: .report(viewModel: reportViewModel),
from: dependency,
transition: .modal(animated: true, completion: nil)
@ -246,7 +238,7 @@ extension DataSourceFacade {
user: user
)
guard let activityViewController = _activityViewController else { return }
dependency.coordinator.present(
_ = dependency.coordinator.present(
scene: .activityViewController(
activityViewController: activityViewController,
sourceView: menuContext.button,
@ -270,8 +262,7 @@ extension DataSourceFacade {
Task {
try await DataSourceFacade.responseToDeleteStatus(
dependency: dependency,
status: status,
authenticationBox: authenticationBox
status: status
)
} // end Task
}

View File

@ -8,10 +8,11 @@
import Foundation
import CoreData
import CoreDataStack
import MastodonCore
extension DataSourceFacade {
static func coordinateToStatusThreadScene(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
target: StatusTarget,
status: ManagedObjectRecord<Status>
) async {
@ -39,14 +40,15 @@ extension DataSourceFacade {
@MainActor
static func coordinateToStatusThreadScene(
provider: DataSourceProvider,
provider: DataSourceProvider & AuthContextProvider,
root: StatusItem.Thread
) async {
let threadViewModel = ThreadViewModel(
context: provider.context,
authContext: provider.authContext,
optionalRoot: root
)
provider.coordinator.present(
_ = provider.coordinator.present(
scene: .thread(viewModel: threadViewModel),
from: provider,
transition: .show

View File

@ -7,18 +7,18 @@
import UIKit
import MetaTextKit
import MastodonUI
import CoreDataStack
import MastodonCore
import MastodonUI
// MARK: - Notification AuthorMenuAction
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
notificationView: NotificationView,
menuButton button: UIButton,
didSelectAction action: MastodonMenu.Action
) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else {
@ -47,15 +47,14 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
status: nil,
button: button,
barButtonItem: nil
),
authenticationBox: authenticationBox
)
)
} // end Task
}
}
// MARK: - Notification Author Avatar
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
notificationView: NotificationView,
@ -88,7 +87,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - Follow Request
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
@ -106,15 +105,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
return
}
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
try await DataSourceFacade.responseToUserFollowRequestAction(
dependency: self,
notification: notification,
query: .accept,
authenticationBox: authenticationBox
query: .accept
)
} // end Task
}
@ -135,15 +129,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
return
}
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
try await DataSourceFacade.responseToUserFollowRequestAction(
dependency: self,
notification: notification,
query: .reject,
authenticationBox: authenticationBox
query: .reject
)
} // end Task
}
@ -151,7 +140,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - Status Content
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
notificationView: NotificationView,
@ -279,7 +268,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med
}
// MARK: - Status Toolbar
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
notificationView: NotificationView,
@ -287,7 +276,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
buttonDidPressed button: UIButton,
action: ActionToolbarContainer.Action
) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else {
@ -311,7 +299,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
provider: self,
status: status,
action: action,
authenticationBox: authenticationBox,
sender: button
)
} // end Task
@ -319,7 +306,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - Status Author Avatar
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
notificationView: NotificationView,
@ -354,7 +341,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - Status Content
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
@ -530,7 +517,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: a11y
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) {
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)

View File

@ -8,10 +8,11 @@
import UIKit
import CoreDataStack
import MetaTextKit
import MastodonCore
import MastodonUI
// MARK: - header
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
@ -64,7 +65,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - avatar button
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
@ -92,7 +93,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - content
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
@ -169,7 +170,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & MediaPrev
// MARK: - poll
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
@ -177,7 +178,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
pollTableView tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
guard let pollItem = pollTableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return }
@ -226,7 +226,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
_ = try await context.apiService.vote(
poll: poll,
choices: [choice],
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choice) success")
} catch {
@ -248,7 +248,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
statusView: StatusView,
pollVoteButtonPressed button: UIButton
) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return }
guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return }
guard case let .option(firstPollOption) = firstPollItem else { return }
@ -284,7 +283,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
_ = try await context.apiService.vote(
poll: poll,
choices: choices,
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): vote poll for \(choices) success")
} catch {
@ -303,7 +302,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - toolbar
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
statusView: StatusView,
@ -311,7 +310,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
buttonDidPressed button: UIButton,
action: ActionToolbarContainer.Action
) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else {
@ -327,7 +325,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
provider: self,
status: status,
action: action,
authenticationBox: authenticationBox,
sender: button
)
} // end Task
@ -336,14 +333,13 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - menu button
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(
_ cell: UITableViewCell,
statusView: StatusView,
menuButton button: UIButton,
didSelectAction action: MastodonMenu.Action
) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else {
@ -372,8 +368,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
status: status,
button: button,
barButtonItem: nil
),
authenticationBox: authenticationBox
)
)
} // end Task
}
@ -475,7 +470,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: - StatusMetricView
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) {
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
@ -489,6 +484,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
let userListViewModel = UserListViewModel(
context: context,
authContext: authContext,
kind: .rebloggedBy(status: status)
)
await coordinator.present(
@ -512,6 +508,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
let userListViewModel = UserListViewModel(
context: context,
authContext: authContext,
kind: .favoritedBy(status: status)
)
await coordinator.present(
@ -524,7 +521,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: a11y
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider {
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) {
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)

View File

@ -8,6 +8,7 @@
import os.log
import UIKit
import CoreDataStack
import MastodonCore
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & StatusTableViewControllerNavigateableRelay {
@ -30,7 +31,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
}
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider {
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
func statusKeyCommandHandler(_ sender: UIKeyCommand) {
guard let rawValue = sender.propertyList as? String,
@ -53,7 +54,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
}
// status coordinate
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider {
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
@MainActor
private func statusRecord() async -> ManagedObjectRecord<Status>? {
@ -93,14 +94,13 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
private func replyStatus() async {
guard let status = await statusRecord() else { return }
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
selectionFeedbackGenerator.selectionChanged()
let composeViewModel = ComposeViewModel(
context: self.context,
composeKind: .reply(status: status),
authenticationBox: authenticationBox
authContext: authContext
)
self.coordinator.present(
scene: .compose(viewModel: composeViewModel),
@ -144,19 +144,16 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
}
// toggle
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider {
extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
@MainActor
private func toggleReblog() async {
guard let status = await statusRecord() else { return }
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do {
try await DataSourceFacade.responseToStatusReblogAction(
provider: self,
status: status,
authenticationBox: authenticationBox
status: status
)
} catch {
assertionFailure()
@ -167,13 +164,10 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
private func toggleFavorite() async {
guard let status = await statusRecord() else { return }
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do {
try await DataSourceFacade.responseToStatusFavoriteAction(
provider: self,
status: status,
authenticationBox: authenticationBox
status: status
)
} catch {
assertionFailure()

View File

@ -7,6 +7,7 @@
import os.log
import UIKit
import MastodonCore
extension TableViewControllerNavigateableCore where Self: TableViewControllerNavigateableRelay {
var navigationKeyCommands: [UIKeyCommand] {
@ -124,7 +125,7 @@ extension TableViewControllerNavigateableCore {
}
extension TableViewControllerNavigateableCore where Self: DataSourceProvider {
extension TableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider {
func open() {
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return }
let source = DataSourceItem.Source(indexPath: indexPathForSelectedRow)

View File

@ -12,7 +12,7 @@ import MastodonCore
import MastodonUI
import MastodonLocalization
extension UITableViewDelegate where Self: DataSourceProvider {
extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider {
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)")

View File

@ -5,6 +5,7 @@
// Created by Cirno MainasuK on 2021-9-13.
//
import os.log
import UIKit
import Combine
import CoreData
@ -14,43 +15,43 @@ import MastodonMeta
import MastodonCore
import MastodonUI
final class AccountListViewModel {
final class AccountListViewModel: NSObject {
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
let authContext: AuthContext
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
// output
let authentications = CurrentValueSubject<[Item], Never>([])
let activeMastodonUserObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil)
@Published var authentications: [ManagedObjectRecord<MastodonAuthentication>] = []
@Published var items: [Item] = []
let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
var diffableDataSource: UITableViewDiffableDataSource<Section, Item>!
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
Publishers.CombineLatest(
context.authenticationService.mastodonAuthentications,
context.authenticationService.activeMastodonAuthentication
self.authContext = authContext
self.mastodonAuthenticationFetchedResultsController = {
let fetchRequest = MastodonAuthentication.sortedFetchRequest
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.fetchBatchSize = 20
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context.managedObjectContext,
sectionNameKeyPath: nil,
cacheName: nil
)
.sink { [weak self] authentications, activeAuthentication in
guard let self = self else { return }
var items: [Item] = []
var activeMastodonUserObjectID: NSManagedObjectID?
for authentication in authentications {
let item = Item.authentication(objectID: authentication.objectID)
items.append(item)
if authentication === activeAuthentication {
activeMastodonUserObjectID = authentication.user.objectID
}
}
self.authentications.value = items
self.activeMastodonUserObjectID.value = activeMastodonUserObjectID
}
.store(in: &disposeBag)
return controller
}()
super.init()
// end init
authentications
mastodonAuthenticationFetchedResultsController.delegate = self
$authentications
.receive(on: DispatchQueue.main)
.sink { [weak self] authentications in
guard let self = self else { return }
@ -58,7 +59,10 @@ final class AccountListViewModel {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(authentications, toSection: .main)
let authenticationItems: [Item] = authentications.map {
Item.authentication(record: $0)
}
snapshot.appendItems(authenticationItems, toSection: .main)
snapshot.appendItems([.addAccount], toSection: .main)
diffableDataSource.apply(snapshot) {
@ -76,7 +80,7 @@ extension AccountListViewModel {
}
enum Item: Hashable {
case authentication(objectID: NSManagedObjectID)
case authentication(record: ManagedObjectRecord<MastodonAuthentication>)
case addAccount
}
@ -86,14 +90,17 @@ extension AccountListViewModel {
) {
diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
switch item {
case .authentication(let objectID):
let authentication = managedObjectContext.object(with: objectID) as! MastodonAuthentication
case .authentication(let record):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
if let authentication = record.object(in: managedObjectContext),
let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)
{
AccountListViewModel.configure(
cell: cell,
authentication: authentication,
activeMastodonUserObjectID: self.activeMastodonUserObjectID.eraseToAnyPublisher()
activeAuthentication: activeAuthentication
)
}
return cell
case .addAccount:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell
@ -109,7 +116,7 @@ extension AccountListViewModel {
static func configure(
cell: AccountListTableViewCell,
authentication: MastodonAuthentication,
activeMastodonUserObjectID: AnyPublisher<NSManagedObjectID?, Never>
activeAuthentication: MastodonAuthentication
) {
let user = authentication.user
@ -138,19 +145,14 @@ extension AccountListViewModel {
cell.badgeButton.setBadge(number: count)
// checkmark
activeMastodonUserObjectID
.receive(on: DispatchQueue.main)
.sink { objectID in
let isCurrentUser = user.objectID == objectID
let isActive = activeAuthentication.userID == authentication.userID
cell.tintColor = .label
cell.checkmarkImageView.isHidden = !isCurrentUser
if isCurrentUser {
cell.checkmarkImageView.isHidden = !isActive
if isActive {
cell.accessibilityTraits.insert(.selected)
} else {
cell.accessibilityTraits.remove(.selected)
}
}
.store(in: &cell.disposeBag)
cell.accessibilityLabel = [
cell.nameLabel.text,
@ -161,3 +163,21 @@ extension AccountListViewModel {
.joined(separator: " ")
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension AccountListViewModel: NSFetchedResultsControllerDelegate {
public func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller === mastodonAuthenticationFetchedResultsController else {
assertionFailure()
return
}
authentications = mastodonAuthenticationFetchedResultsController.fetchedObjects?.compactMap { $0.asRecrod } ?? []
}
}

View File

@ -22,7 +22,7 @@ final class AccountListViewController: UIViewController, NeedsDependency {
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
private(set) lazy var viewModel = AccountListViewModel(context: context)
var viewModel: AccountListViewModel!
private(set) lazy var addBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(
@ -64,7 +64,10 @@ extension AccountListViewController: PanModalPresentable {
return .contentHeight(CGFloat(height))
}
let count = viewModel.context.authenticationService.mastodonAuthentications.value.count + 1
let request = MastodonAuthentication.sortedFetchRequest
let authenticationCount = (try? context.managedObjectContext.count(for: request)) ?? 0
let count = authenticationCount + 1
let height = calculateHeight(of: count)
return .contentHeight(height)
}
@ -174,16 +177,14 @@ extension AccountListViewController: UITableViewDelegate {
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .authentication(let objectID):
case .authentication(let record):
assert(Thread.isMainThread)
let authentication = context.managedObjectContext.object(with: objectID) as! MastodonAuthentication
context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
guard let authentication = record.object(in: context.managedObjectContext) else { return }
Task { @MainActor in
let isActive = try await context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
guard isActive else { return }
self.coordinator.setup()
}
.store(in: &disposeBag)
} // end Task
case .addAccount:
// TODO: add dismiss entry for welcome scene
coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))

View File

@ -134,11 +134,6 @@ extension AutoCompleteViewModel.State {
return
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
await enter(state: Fail.self)
return
}
let searchText = viewModel.inputText.value
let searchType = AutoCompleteViewModel.SearchType(inputText: searchText) ?? .default
@ -154,7 +149,7 @@ extension AutoCompleteViewModel.State {
do {
let response = try await viewModel.context.apiService.search(
query: query,
authenticationBox: authenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
await enter(state: Idle.self)

View File

@ -17,6 +17,7 @@ final class AutoCompleteViewModel {
// input
let context: AppContext
let authContext: AuthContext
public let inputText = CurrentValueSubject<String, Never>("") // contains "@" or "#" prefix
public let symbolBoundingRect = CurrentValueSubject<CGRect, Never>(.zero)
public let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
@ -36,8 +37,9 @@ final class AutoCompleteViewModel {
return stateMachine
}()
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
autoCompleteItems
.receive(on: DispatchQueue.main)

View File

@ -137,7 +137,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
private(set) lazy var autoCompleteViewController: AutoCompleteViewController = {
let viewController = AutoCompleteViewController()
viewController.viewModel = AutoCompleteViewModel(context: context)
viewController.viewModel = AutoCompleteViewModel(context: context, authContext: viewModel.authContext)
viewController.delegate = self
viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel
return viewController

View File

@ -29,8 +29,11 @@ final class ComposeViewModel: NSObject {
// input
let context: AppContext
let composeKind: ComposeStatusSection.ComposeKind
let authenticationBox: MastodonAuthenticationBox
let authContext: AuthContext
var authenticationBox: MastodonAuthenticationBox {
authContext.mastodonAuthenticationBox
}
@Published var isPollComposing = false
@Published var isCustomEmojiComposing = false
@ -116,11 +119,12 @@ final class ComposeViewModel: NSObject {
init(
context: AppContext,
composeKind: ComposeStatusSection.ComposeKind,
authenticationBox: MastodonAuthenticationBox
authContext: AuthContext
) {
self.context = context
self.composeKind = composeKind
self.authenticationBox = authenticationBox
self.authContext = authContext
self.title = {
switch composeKind {
case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost
@ -130,8 +134,7 @@ final class ComposeViewModel: NSObject {
self.selectedStatusVisibility = {
// default private when user locked
var visibility: ComposeToolbarView.VisibilitySelectionType = {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value,
let author = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
else {
return .public
}
@ -168,15 +171,12 @@ final class ComposeViewModel: NSObject {
self.instanceConfiguration = {
var configuration: Mastodon.Entity.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
guard let authentication = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)
else {
return
}
guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) else { return }
configuration = authentication.instance?.configuration
}
return configuration
}()
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authenticationBox.domain)
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain)
super.init()
// end init

View File

@ -116,6 +116,11 @@ extension DiscoveryCommunityViewController {
}
// MARK: - AuthContextProvider
extension DiscoveryCommunityViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension DiscoveryCommunityViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:CommunityViewController.AutoGenerateTableViewDelegate

View File

@ -18,6 +18,7 @@ extension DiscoveryCommunityViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,

View File

@ -136,11 +136,6 @@ extension DiscoveryCommunityViewModel.State {
break
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self)
return
}
let maxID = self.maxID
let isReloading = maxID == nil
@ -156,7 +151,7 @@ extension DiscoveryCommunityViewModel.State {
minID: nil,
limit: 20
),
authenticationBox: authenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
let newMaxID = response.link?.maxID

View File

@ -22,6 +22,7 @@ final class DiscoveryCommunityViewModel {
// input
let context: AppContext
let authContext: AuthContext
let viewDidAppeared = PassthroughSubject<Void, Never>()
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -43,20 +44,15 @@ final class DiscoveryCommunityViewModel {
let didLoadLatest = PassthroughSubject<Void, Never>()
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// end init
context.authenticationService.activeMastodonAuthentication
.map { $0?.domain }
.assign(to: \.domain, on: statusFetchedResultsController)
.store(in: &disposeBag)
}
deinit {

View File

@ -26,10 +26,7 @@ public class DiscoveryViewController: TabmanViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
private(set) lazy var viewModel = DiscoveryViewModel(
context: context,
coordinator: coordinator
)
var viewModel: DiscoveryViewModel!
private(set) lazy var buttonBar: TMBar.ButtonBar = {
let buttonBar = TMBar.ButtonBar()

View File

@ -18,6 +18,7 @@ final class DiscoveryViewModel {
// input
let context: AppContext
let authContext: AuthContext
let discoveryPostsViewController: DiscoveryPostsViewController
let discoveryHashtagsViewController: DiscoveryHashtagsViewController
let discoveryNewsViewController: DiscoveryNewsViewController
@ -26,41 +27,43 @@ final class DiscoveryViewModel {
@Published var viewControllers: [ScrollViewContainer & PageViewController]
init(context: AppContext, coordinator: SceneCoordinator) {
init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) {
self.context = context
self.authContext = authContext
func setupDependency(_ needsDependency: NeedsDependency) {
needsDependency.context = context
needsDependency.coordinator = coordinator
}
self.context = context
discoveryPostsViewController = {
let viewController = DiscoveryPostsViewController()
setupDependency(viewController)
viewController.viewModel = DiscoveryPostsViewModel(context: context)
viewController.viewModel = DiscoveryPostsViewModel(context: context, authContext: authContext)
return viewController
}()
discoveryHashtagsViewController = {
let viewController = DiscoveryHashtagsViewController()
setupDependency(viewController)
viewController.viewModel = DiscoveryHashtagsViewModel(context: context)
viewController.viewModel = DiscoveryHashtagsViewModel(context: context, authContext: authContext)
return viewController
}()
discoveryNewsViewController = {
let viewController = DiscoveryNewsViewController()
setupDependency(viewController)
viewController.viewModel = DiscoveryNewsViewModel(context: context)
viewController.viewModel = DiscoveryNewsViewModel(context: context, authContext: authContext)
return viewController
}()
discoveryCommunityViewController = {
let viewController = DiscoveryCommunityViewController()
setupDependency(viewController)
viewController.viewModel = DiscoveryCommunityViewModel(context: context)
viewController.viewModel = DiscoveryCommunityViewModel(context: context, authContext: authContext)
return viewController
}()
discoveryForYouViewController = {
let viewController = DiscoveryForYouViewController()
setupDependency(viewController)
viewController.viewModel = DiscoveryForYouViewModel(context: context)
viewController.viewModel = DiscoveryForYouViewModel(context: context, authContext: authContext)
return viewController
}()
self.viewControllers = [

View File

@ -101,6 +101,11 @@ extension DiscoveryForYouViewController {
}
// MARK: - AuthContextProvider
extension DiscoveryForYouViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension DiscoveryForYouViewController: UITableViewDelegate {
@ -110,9 +115,10 @@ extension DiscoveryForYouViewController: UITableViewDelegate {
guard let user = record.object(in: context.managedObjectContext) else { return }
let profileViewModel = CachedProfileViewModel(
context: context,
authContext: viewModel.authContext,
mastodonUser: user
)
coordinator.present(
_ = coordinator.present(
scene: .profile(viewModel: profileViewModel),
from: self,
transition: .show
@ -128,15 +134,13 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
profileCardView: ProfileCardView,
relationshipButtonDidPressed button: ProfileRelationshipActionButton
) {
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
Task {
try await DataSourceFacade.responseToUserFollowAction(
dependency: self,
user: record,
authenticationBox: authenticationBox
user: record
)
} // end Task
}
@ -157,9 +161,9 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
return
}
let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context)
let familiarFollowersViewModel = FamiliarFollowersViewModel(context: context, authContext: authContext)
familiarFollowersViewModel.familiarFollowers = familiarFollowers
coordinator.present(
_ = coordinator.present(
scene: .familiarFollowers(viewModel: familiarFollowersViewModel),
from: self,
transition: .show

View File

@ -19,6 +19,7 @@ extension DiscoveryForYouViewModel {
tableView: tableView,
context: context,
configuration: DiscoverySection.Configuration(
authContext: authContext,
profileCardTableViewCellDelegate: profileCardTableViewCellDelegate,
familiarFollowers: $familiarFollowers
)

View File

@ -20,6 +20,7 @@ final class DiscoveryForYouViewModel {
// input
let context: AppContext
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
@MainActor
@ -30,19 +31,15 @@ final class DiscoveryForYouViewModel {
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
let didLoadLatest = PassthroughSubject<Void, Never>()
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
domain: authContext.mastodonAuthenticationBox.domain,
additionalPredicate: nil
)
// end init
context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.domain, on: userFetchedResultsController)
.store(in: &disposeBag)
}
deinit {
@ -59,16 +56,12 @@ extension DiscoveryForYouViewModel {
isFetching = true
defer { isFetching = false }
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
throw APIService.APIError.implicit(.badRequest)
}
do {
let userIDs = try await fetchSuggestionAccounts()
let _familiarFollowersResponse = try? await context.apiService.familiarFollowers(
query: .init(ids: userIDs),
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
familiarFollowers = _familiarFollowersResponse?.value ?? []
userFetchedResultsController.userIDs = userIDs
@ -78,14 +71,10 @@ extension DiscoveryForYouViewModel {
}
private func fetchSuggestionAccounts() async throws -> [Mastodon.Entity.Account.ID] {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
throw APIService.APIError.implicit(.badRequest)
}
do {
let response = try await context.apiService.suggestionAccountV2(
query: nil,
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
let userIDs = response.value.map { $0.account.id }
return userIDs
@ -93,7 +82,7 @@ extension DiscoveryForYouViewModel {
// fallback V1
let response = try await context.apiService.suggestionAccount(
query: nil,
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
let userIDs = response.value.map { $0.id }
return userIDs

View File

@ -107,7 +107,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(indexPath)")
guard case let .hashtag(tag) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name)
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: tag.name)
coordinator.present(
scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel),
from: self,
@ -217,7 +217,7 @@ extension DiscoveryHashtagsViewController: TableViewControllerNavigateable {
guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return }
guard case let .hashtag(tag) = item else { return }
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name)
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: tag.name)
coordinator.present(
scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel),
from: self,

View File

@ -15,7 +15,7 @@ extension DiscoveryHashtagsViewModel {
diffableDataSource = DiscoverySection.diffableDataSource(
tableView: tableView,
context: context,
configuration: DiscoverySection.Configuration()
configuration: DiscoverySection.Configuration(authContext: authContext)
)
var snapshot = NSDiffableDataSourceSnapshot<DiscoverySection, DiscoveryItem>()

View File

@ -22,26 +22,22 @@ final class DiscoveryHashtagsViewModel {
// input
let context: AppContext
let authContext: AuthContext
let viewDidAppeared = PassthroughSubject<Void, Never>()
// output
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
@Published var hashtags: [Mastodon.Entity.Tag] = []
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
// end init
Publishers.CombineLatest(
context.authenticationService.activeMastodonAuthenticationBox,
viewDidAppeared
)
.compactMap { authenticationBox, _ -> MastodonAuthenticationBox? in
return authenticationBox
}
.throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
.asyncMap { authenticationBox in
try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil)
}
.retry(3)
.map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
@ -69,8 +65,7 @@ extension DiscoveryHashtagsViewModel {
@MainActor
func fetch() async throws {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let response = try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
let response = try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil)
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)")
}

View File

@ -16,7 +16,7 @@ extension DiscoveryNewsViewModel {
diffableDataSource = DiscoverySection.diffableDataSource(
tableView: tableView,
context: context,
configuration: DiscoverySection.Configuration()
configuration: DiscoverySection.Configuration(authContext: authContext)
)
stateMachine.enter(State.Reloading.self)

View File

@ -137,18 +137,13 @@ extension DiscoveryNewsViewModel.State {
break
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self)
return
}
let offset = self.offset
let isReloading = offset == nil
Task {
do {
let response = try await viewModel.context.apiService.trendLinks(
domain: authenticationBox.domain,
domain: viewModel.authContext.mastodonAuthenticationBox.domain,
query: Mastodon.API.Trends.StatusQuery(
offset: offset,
limit: nil

View File

@ -20,6 +20,7 @@ final class DiscoveryNewsViewModel {
// input
let context: AppContext
let authContext: AuthContext
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@ -41,8 +42,9 @@ final class DiscoveryNewsViewModel {
let didLoadLatest = PassthroughSubject<Void, Never>()
@Published var isServerSupportEndpoint = true
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
// end init
Task {
@ -59,11 +61,9 @@ final class DiscoveryNewsViewModel {
extension DiscoveryNewsViewModel {
func checkServerEndpoint() async {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do {
_ = try await context.apiService.trendLinks(
domain: authenticationBox.domain,
domain: authContext.mastodonAuthenticationBox.domain,
query: .init(offset: nil, limit: nil)
)
} catch let error as Mastodon.API.Error where error.httpResponseStatus.code == 404 {

View File

@ -128,6 +128,11 @@ extension DiscoveryPostsViewController {
}
// MARK: - AuthContextProvider
extension DiscoveryPostsViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension DiscoveryPostsViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:DiscoveryPostsViewController.AutoGenerateTableViewDelegate

View File

@ -18,6 +18,7 @@ extension DiscoveryPostsViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,

View File

@ -137,18 +137,13 @@ extension DiscoveryPostsViewModel.State {
break
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self)
return
}
let offset = self.offset
let isReloading = offset == nil
Task {
do {
let response = try await viewModel.context.apiService.trendStatuses(
domain: authenticationBox.domain,
domain: viewModel.authContext.mastodonAuthenticationBox.domain,
query: Mastodon.API.Trends.StatusQuery(
offset: offset,
limit: nil

View File

@ -20,6 +20,7 @@ final class DiscoveryPostsViewModel {
// input
let context: AppContext
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -41,20 +42,16 @@ final class DiscoveryPostsViewModel {
let didLoadLatest = PassthroughSubject<Void, Never>()
@Published var isServerSupportEndpoint = true
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// end init
context.authenticationService.activeMastodonAuthentication
.map { $0?.domain }
.assign(to: \.domain, on: statusFetchedResultsController)
.store(in: &disposeBag)
Task {
await checkServerEndpoint()
} // end Task
@ -68,11 +65,9 @@ final class DiscoveryPostsViewModel {
extension DiscoveryPostsViewModel {
func checkServerEndpoint() async {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do {
_ = try await context.apiService.trendStatuses(
domain: authenticationBox.domain,
domain: authContext.mastodonAuthenticationBox.domain,
query: .init(offset: nil, limit: nil)
)
} catch let error as Mastodon.API.Error where error.httpResponseStatus.code == 404 {

View File

@ -166,17 +166,21 @@ extension HashtagTimelineViewController {
@objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let composeViewModel = ComposeViewModel(
context: context,
composeKind: .hashtag(hashtag: viewModel.hashtag),
authenticationBox: authenticationBox
authContext: viewModel.authContext
)
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
}
// MARK: - AuthContextProvider
extension HashtagTimelineViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension HashtagTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:HashtagTimelineViewController.AutoGenerateTableViewDelegate

View File

@ -20,6 +20,7 @@ extension HashtagTimelineViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,

View File

@ -135,12 +135,6 @@ extension HashtagTimelineViewModel.State {
break
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
assertionFailure()
stateMachine.enter(Fail.self)
return
}
// TODO: only set large count when using Wi-Fi
let maxID = self.maxID
let isReloading = maxID == nil
@ -148,10 +142,10 @@ extension HashtagTimelineViewModel.State {
Task {
do {
let response = try await viewModel.context.apiService.hashtagTimeline(
domain: authenticationBox.domain,
domain: viewModel.authContext.mastodonAuthenticationBox.domain,
maxID: maxID,
hashtag: viewModel.hashtag,
authenticationBox: authenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
let newMaxID: String? = {

View File

@ -26,6 +26,7 @@ final class HashtagTimelineViewModel {
// input
let context: AppContext
let authContext: AuthContext
let fetchedResultsController: StatusFetchedResultsController
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
@ -52,20 +53,16 @@ final class HashtagTimelineViewModel {
}()
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<State?, Never>(nil)
init(context: AppContext, hashtag: String) {
init(context: AppContext, authContext: AuthContext, hashtag: String) {
self.context = context
self.authContext = authContext
self.hashtag = hashtag
self.fetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
// end init
context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.domain, on: fetchedResultsController)
.store(in: &disposeBag)
}
deinit {

View File

@ -81,8 +81,11 @@ extension HomeTimelineViewController {
},
UIAction(title: "Account Recommend", image: UIImage(systemName: "human"), attributes: []) { [weak self] action in
guard let self = self else { return }
let suggestionAccountViewModel = SuggestionAccountViewModel(context: self.context)
self.coordinator.present(
let suggestionAccountViewModel = SuggestionAccountViewModel(
context: self.context,
authContext: self.viewModel.authContext
)
_ = self.coordinator.present(
scene: .suggestionAccount(viewModel: suggestionAccountViewModel),
from: self,
transition: .modal(animated: true, completion: nil)
@ -150,7 +153,7 @@ extension HomeTimelineViewController {
children: [
UIAction(title: "Badge +1", image: UIImage(systemName: "app.badge.fill"), attributes: []) { [weak self] action in
guard let self = self else { return }
guard let accessToken = self.context.authenticationService.activeMastodonAuthentication.value?.userAccessToken else { return }
let accessToken = self.viewModel.authContext.mastodonAuthenticationBox.userAuthorization.accessToken
UserDefaults.shared.increaseNotificationCount(accessToken: accessToken)
self.context.notificationService.applicationIconBadgeNeedsUpdate.send()
},
@ -333,7 +336,8 @@ extension HomeTimelineViewController {
}
@objc private func showAccountList(_ sender: UIAction) {
coordinator.present(scene: .accountList, from: self, transition: .modal(animated: true, completion: nil))
let accountListViewModel = AccountListViewModel(context: context, authContext: viewModel.authContext)
coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
@objc private func showProfileAction(_ sender: UIAction) {
@ -342,7 +346,7 @@ extension HomeTimelineViewController {
let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in
guard let self = self else { return }
guard let textField = alertController?.textFields?.first else { return }
let profileViewModel = RemoteProfileViewModel(context: self.context, userID: textField.text ?? "")
let profileViewModel = RemoteProfileViewModel(context: self.context, authContext: self.viewModel.authContext, userID: textField.text ?? "")
self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show)
}
alertController.addAction(showAction)
@ -357,7 +361,7 @@ extension HomeTimelineViewController {
let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in
guard let self = self else { return }
guard let textField = alertController?.textFields?.first else { return }
let threadViewModel = RemoteThreadViewModel(context: self.context, statusID: textField.text ?? "")
let threadViewModel = RemoteThreadViewModel(context: self.context, authContext: self.viewModel.authContext, statusID: textField.text ?? "")
self.coordinator.present(scene: .thread(viewModel: threadViewModel), from: self, transition: .show)
}
alertController.addAction(showAction)
@ -367,8 +371,6 @@ extension HomeTimelineViewController {
}
private func showNotification(_ sender: UIAction, notificationType: Mastodon.Entity.Notification.NotificationType) {
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let alertController = UIAlertController(title: "Enter notification ID", message: nil, preferredStyle: .alert)
alertController.addTextField()
@ -380,7 +382,7 @@ extension HomeTimelineViewController {
else { return }
let pushNotification = MastodonPushNotification(
accessToken: authenticationBox.userAuthorization.accessToken,
accessToken: self.viewModel.authContext.mastodonAuthenticationBox.userAuthorization.accessToken,
notificationID: notificationID,
notificationType: notificationType.rawValue,
preferredLocale: nil,
@ -393,7 +395,7 @@ extension HomeTimelineViewController {
alertController.addAction(showAction)
// for multiple accounts debug
let boxes = self.context.authenticationService.mastodonAuthenticationBoxes.value // already sorted
let boxes = self.context.authenticationService.mastodonAuthenticationBoxes // already sorted
if boxes.count >= 2 {
let accessToken = boxes[1].userAuthorization.accessToken
let showForSecondaryAction = UIAlertAction(title: "Show for Secondary", style: .default) { [weak self, weak alertController] _ in
@ -420,12 +422,20 @@ extension HomeTimelineViewController {
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil))
_ = self.coordinator.present(
scene: .alertController(alertController: alertController),
from: self,
transition: .alertController(animated: true, completion: nil)
)
}
@objc private func showSettings(_ sender: UIAction) {
guard let currentSetting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, setting: currentSetting)
let settingsViewModel = SettingsViewModel(
context: context,
authContext: viewModel.authContext,
setting: currentSetting
)
coordinator.present(
scene: .settings(viewModel: settingsViewModel),
from: self,

View File

@ -28,7 +28,7 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
private(set) lazy var viewModel = HomeTimelineViewModel(context: context)
var viewModel: HomeTimelineViewModel!
let mediaPreviewTransitionController = MediaPreviewTransitionController()
@ -373,9 +373,9 @@ extension HomeTimelineViewController {
extension HomeTimelineViewController {
@objc private func findPeopleButtonPressed(_ sender: PrimaryActionButton) {
let suggestionAccountViewModel = SuggestionAccountViewModel(context: context)
let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: viewModel.authContext)
suggestionAccountViewModel.delegate = viewModel
coordinator.present(
_ = coordinator.present(
scene: .suggestionAccount(viewModel: suggestionAccountViewModel),
from: self,
transition: .modal(animated: true, completion: nil)
@ -391,7 +391,7 @@ extension HomeTimelineViewController {
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, setting: setting)
let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting)
coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
@ -403,12 +403,8 @@ extension HomeTimelineViewController {
}
@objc func signOutAction(_ sender: UIAction) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
Task { @MainActor in
try await context.authenticationService.signOutMastodonUser(authenticationBox: authenticationBox)
try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
self.coordinator.setup()
}
}
@ -492,6 +488,11 @@ extension HomeTimelineViewController {
}
}
// MARK: - AuthContextProvider
extension HomeTimelineViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:HomeTimelineViewController.AutoGenerateTableViewDelegate

View File

@ -21,6 +21,7 @@ extension HomeTimelineViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
filterContext: .home,

View File

@ -63,11 +63,6 @@ extension HomeTimelineViewModel.LoadLatestState {
override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
// sign out when loading will enter here
stateMachine.enter(Fail.self)
return
}
let latestFeedRecords = viewModel.fetchedResultsController.records.prefix(APIService.onceRequestStatusMaxCount)
let parentManagedObjectContext = viewModel.fetchedResultsController.fetchedResultsController.managedObjectContext
@ -85,7 +80,7 @@ extension HomeTimelineViewModel.LoadLatestState {
do {
let response = try await viewModel.context.apiService.homeTimeline(
authenticationBox: activeMastodonAuthenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
await enter(state: Idle.self)

View File

@ -64,11 +64,6 @@ extension HomeTimelineViewModel.LoadOldestState {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
assertionFailure()
stateMachine.enter(Fail.self)
return
}
guard let lastFeedRecord = viewModel.fetchedResultsController.records.last else {
stateMachine.enter(Idle.self)
@ -92,7 +87,7 @@ extension HomeTimelineViewModel.LoadOldestState {
do {
let response = try await viewModel.context.apiService.homeTimeline(
maxID: maxID,
authenticationBox: activeMastodonAuthenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
let statuses = response.value

View File

@ -26,6 +26,7 @@ final class HomeTimelineViewModel: NSObject {
// input
let context: AppContext
let authContext: AuthContext
let fetchedResultsController: FeedFetchedResultsController
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -76,25 +77,17 @@ final class HomeTimelineViewModel: NSObject {
var cellFrameCache = NSCache<NSNumber, NSValue>()
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext)
self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context)
super.init()
context.authenticationService.activeMastodonAuthenticationBox
.sink { [weak self] authenticationBox in
guard let self = self else { return }
guard let authenticationBox = authenticationBox else {
self.fetchedResultsController.predicate = Feed.predicate(kind: .none, acct: .none)
return
}
self.fetchedResultsController.predicate = Feed.predicate(
fetchedResultsController.predicate = Feed.predicate(
kind: .home,
acct: .mastodon(domain: authenticationBox.domain, userID: authenticationBox.userID)
acct: .mastodon(domain: authContext.mastodonAuthenticationBox.domain, userID: authContext.mastodonAuthenticationBox.userID)
)
}
.store(in: &disposeBag)
homeTimelineNeedRefresh
.sink { [weak self] _ in
@ -131,7 +124,6 @@ extension HomeTimelineViewModel {
// load timeline gap
func loadMore(item: StatusItem) async {
guard case let .feedLoader(record) = item else { return }
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let diffableDataSource = diffableDataSource else { return }
var snapshot = diffableDataSource.snapshot()
@ -169,7 +161,7 @@ extension HomeTimelineViewModel {
let maxID = status.id
_ = try await context.apiService.homeTimeline(
maxID: maxID,
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
} catch {
do {

View File

@ -147,6 +147,11 @@ extension NotificationTimelineViewController {
}
// MARK: - AuthContextProvider
extension NotificationTimelineViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension NotificationTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:NotificationTimelineViewController.AutoGenerateTableViewDelegate
@ -297,9 +302,10 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
if let stauts = notification.status {
let threadViewModel = ThreadViewModel(
context: self.context,
authContext: self.viewModel.authContext,
optionalRoot: .root(context: .init(status: .init(objectID: stauts.objectID)))
)
self.coordinator.present(
_ = self.coordinator.present(
scene: .thread(viewModel: threadViewModel),
from: self,
transition: .show
@ -307,9 +313,10 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable {
} else {
let profileViewModel = ProfileViewModel(
context: self.context,
authContext: self.viewModel.authContext,
optionalMastodonUser: notification.account
)
self.coordinator.present(
_ = self.coordinator.present(
scene: .profile(viewModel: profileViewModel),
from: self,
transition: .show

View File

@ -20,6 +20,7 @@ extension NotificationTimelineViewModel {
tableView: tableView,
context: context,
configuration: NotificationSection.Configuration(
authContext: authContext,
notificationTableViewCellDelegate: notificationTableViewCellDelegate,
filterContext: .notifications,
activeFilters: context.statusFilterService.$activeFilters

View File

@ -63,11 +63,6 @@ extension NotificationTimelineViewModel.LoadOldestState {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
assertionFailure()
stateMachine.enter(Fail.self)
return
}
guard let lastFeedRecord = viewModel.feedFetchedResultsController.records.last else {
stateMachine.enter(Fail.self)
@ -93,7 +88,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
let response = try await viewModel.context.apiService.notifications(
maxID: maxID,
scope: scope,
authenticationBox: authenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
let notifications = response.value

View File

@ -21,6 +21,7 @@ final class NotificationTimelineViewModel {
// input
let context: AppContext
let authContext: AuthContext
let scope: Scope
let feedFetchedResultsController: FeedFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -47,28 +48,19 @@ final class NotificationTimelineViewModel {
init(
context: AppContext,
authContext: AuthContext,
scope: Scope
) {
self.context = context
self.authContext = authContext
self.scope = scope
self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext)
// end init
context.authenticationService.activeMastodonAuthenticationBox
.sink { [weak self] authenticationBox in
guard let self = self else { return }
guard let authenticationBox = authenticationBox else {
self.feedFetchedResultsController.predicate = Feed.nonePredicate()
return
}
let predicate = NotificationTimelineViewModel.feedPredicate(
authenticationBox: authenticationBox,
feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate(
authenticationBox: authContext.mastodonAuthenticationBox,
scope: scope
)
self.feedFetchedResultsController.predicate = predicate
}
.store(in: &disposeBag)
}
deinit {
@ -122,8 +114,6 @@ extension NotificationTimelineViewModel {
// load lastest
func loadLatest() async {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
isLoadingLatest = true
defer { isLoadingLatest = false }
@ -131,7 +121,7 @@ extension NotificationTimelineViewModel {
_ = try await context.apiService.notifications(
maxID: nil,
scope: scope,
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
} catch {
didLoadLatest.send()
@ -142,7 +132,6 @@ extension NotificationTimelineViewModel {
// load timeline gap
func loadMore(item: NotificationItem) async {
guard case let .feedLoader(record) = item else { return }
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let managedObjectContext = context.managedObjectContext
let key = "LoadMore@\(record.objectID)"
@ -163,7 +152,7 @@ extension NotificationTimelineViewModel {
_ = try await context.apiService.notifications(
maxID: maxID,
scope: scope,
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
} catch {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch more failure: \(error.localizedDescription)")

View File

@ -24,7 +24,7 @@ final class NotificationViewController: TabmanViewController, NeedsDependency {
var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>()
private(set) lazy var viewModel = NotificationViewModel(context: context)
var viewModel: NotificationViewModel!
let pageSegmentedControl = UISegmentedControl()
@ -154,6 +154,7 @@ extension NotificationViewController {
viewController.coordinator = coordinator
viewController.viewModel = NotificationTimelineViewModel(
context: context,
authContext: viewModel.authContext,
scope: scope
)
return viewController

View File

@ -19,6 +19,7 @@ final class NotificationViewModel {
// input
let context: AppContext
let authContext: AuthContext
let viewDidLoad = PassthroughSubject<Void, Never>()
// output
@ -27,8 +28,9 @@ final class NotificationViewModel {
@Published var currentPageIndex = 0
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
// end init
}
}

View File

@ -182,9 +182,13 @@ extension MastodonPickServerViewController {
authenticationViewModel
.authenticated
.flatMap { [weak self] (domain, user) -> AnyPublisher<Result<Bool, Error>, Never> in
guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() }
return self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
.asyncMap { domain, user -> Result<Bool, Error> in
do {
let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
return .success(result)
} catch {
return .failure(error)
}
}
.receive(on: DispatchQueue.main)
.sink { [weak self] result in

View File

@ -144,7 +144,7 @@ extension WelcomeViewController {
signUpButton.addTarget(self, action: #selector(signUpButtonDidClicked(_:)), for: .touchUpInside)
signInButton.addTarget(self, action: #selector(signInButtonDidClicked(_:)), for: .touchUpInside)
viewModel.needsShowDismissEntry
viewModel.$needsShowDismissEntry
.receive(on: DispatchQueue.main)
.sink { [weak self] needsShowDismissEntry in
guard let self = self else { return }

View File

@ -17,15 +17,14 @@ final class WelcomeViewModel {
let context: AppContext
// output
let needsShowDismissEntry = CurrentValueSubject<Bool, Never>(false)
@Published var needsShowDismissEntry = false
init(context: AppContext) {
self.context = context
context.authenticationService.mastodonAuthentications
context.authenticationService.$mastodonAuthenticationBoxes
.map { !$0.isEmpty }
.assign(to: \.value, on: needsShowDismissEntry)
.store(in: &disposeBag)
.assign(to: &$needsShowDismissEntry)
}
}

View File

@ -134,6 +134,11 @@ extension BookmarkViewController: UITableViewDelegate, AutoGenerateTableViewDele
// MARK: - StatusTableViewCellDelegate
extension BookmarkViewController: StatusTableViewCellDelegate { }
// MARK: - AuthContextProvider
extension BookmarkViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
extension BookmarkViewController {
override var keyCommands: [UIKeyCommand]? {
return navigationKeyCommands + statusNavigationKeyCommands

View File

@ -17,6 +17,7 @@ extension BookmarkViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,

View File

@ -51,7 +51,7 @@ extension BookmarkViewModel.State {
guard let viewModel = viewModel else { return false }
switch stateClass {
case is Reloading.Type:
return viewModel.activeMastodonAuthenticationBox.value != nil
return true
default:
return false
}
@ -134,20 +134,15 @@ extension BookmarkViewModel.State {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let authenticationBox = viewModel.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self)
return
}
if previousState is Reloading {
maxID = nil
}
Task {
do {
let response = try await viewModel.context.apiService.bookmarkedStatuses(
maxID: maxID,
authenticationBox: authenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
var hasNewStatusesAppend = false

View File

@ -18,7 +18,8 @@ final class BookmarkViewModel {
// input
let context: AppContext
let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -37,23 +38,14 @@ final class BookmarkViewModel {
return stateMachine
}()
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
context.authenticationService.activeMastodonAuthenticationBox
.assign(to: \.value, on: activeMastodonAuthenticationBox)
.store(in: &disposeBag)
activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.domain, on: statusFetchedResultsController)
.store(in: &disposeBag)
}
}

View File

@ -11,8 +11,8 @@ import MastodonCore
final class CachedProfileViewModel: ProfileViewModel {
init(context: AppContext, mastodonUser: MastodonUser) {
super.init(context: context, optionalMastodonUser: mastodonUser)
init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUser) {
super.init(context: context, authContext: authContext, optionalMastodonUser: mastodonUser)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Profile] user[\(mastodonUser.id)] profile: \(mastodonUser.acctWithDomain)")
}

View File

@ -75,6 +75,13 @@ extension FamiliarFollowersViewController {
}
// MARK: - AuthContextProvider
extension FamiliarFollowersViewController: AuthContextProvider {
var authContext: AuthContext {
viewModel.authContext
}
}
// MARK: - UITableViewDelegate
extension FamiliarFollowersViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:FamiliarFollowersViewController.AutoGenerateTableViewDelegate

View File

@ -17,6 +17,7 @@ final class FamiliarFollowersViewModel {
// input
let context: AppContext
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
@Published var familiarFollowers: Mastodon.Entity.FamiliarFollowers?
@ -24,20 +25,16 @@ final class FamiliarFollowersViewModel {
// output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
domain: authContext.mastodonAuthenticationBox.domain,
additionalPredicate: nil
)
// end init
context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.domain, on: userFetchedResultsController)
.store(in: &disposeBag)
$familiarFollowers
.map { familiarFollowers -> [MastodonUser.ID] in
guard let familiarFollowers = familiarFollowers else { return [] }

View File

@ -104,6 +104,11 @@ extension FavoriteViewController {
}
// MARK: - AuthContextProvider
extension FavoriteViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension FavoriteViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:FavoriteViewController.AutoGenerateTableViewDelegate

View File

@ -17,6 +17,7 @@ extension FavoriteViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,

View File

@ -51,7 +51,7 @@ extension FavoriteViewModel.State {
guard let viewModel = viewModel else { return false }
switch stateClass {
case is Reloading.Type:
return viewModel.activeMastodonAuthenticationBox.value != nil
return true
default:
return false
}
@ -134,10 +134,6 @@ extension FavoriteViewModel.State {
super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let authenticationBox = viewModel.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self)
return
}
if previousState is Reloading {
maxID = nil
}
@ -147,7 +143,7 @@ extension FavoriteViewModel.State {
do {
let response = try await viewModel.context.apiService.favoritedStatuses(
maxID: maxID,
authenticationBox: authenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
var hasNewStatusesAppend = false

View File

@ -18,7 +18,7 @@ final class FavoriteViewModel {
// input
let context: AppContext
let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never>
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -37,23 +37,14 @@ final class FavoriteViewModel {
return stateMachine
}()
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: nil,
domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil
)
context.authenticationService.activeMastodonAuthenticationBox
.assign(to: \.value, on: activeMastodonAuthenticationBox)
.store(in: &disposeBag)
activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.domain, on: statusFetchedResultsController)
.store(in: &disposeBag)
}
}

View File

@ -82,8 +82,8 @@ extension FollowerListViewController {
// trigger user timeline loading
Publishers.CombineLatest(
viewModel.domain.removeDuplicates().eraseToAnyPublisher(),
viewModel.userID.removeDuplicates().eraseToAnyPublisher()
viewModel.$domain.removeDuplicates(),
viewModel.$userID.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
@ -101,6 +101,12 @@ extension FollowerListViewController {
}
// MARK: - AuthContextProvider
extension FollowerListViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:FollowerListViewController.AutoGenerateTableViewDelegate

View File

@ -50,10 +50,10 @@ extension FollowerListViewModel {
case is State.Idle, is State.Loading, is State.Fail:
snapshot.appendItems([.bottomLoader], toSection: .main)
case is State.NoMore:
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value,
let userID = self.userID.value,
userID != activeMastodonAuthenticationBox.userID
guard let userID = self.userID,
userID != self.authContext.mastodonAuthenticationBox.userID
else { break }
// display hint footer exclude self
let text = L10n.Scene.Follower.footer
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
default:

View File

@ -51,7 +51,7 @@ extension FollowerListViewModel.State {
guard let viewModel = viewModel else { return false }
switch stateClass {
case is Reloading.Type:
return viewModel.userID.value != nil
return viewModel.userID != nil
default:
return false
}
@ -139,12 +139,7 @@ extension FollowerListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let userID = viewModel.userID.value, !userID.isEmpty else {
stateMachine.enter(Fail.self)
return
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
guard let userID = viewModel.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self)
return
}
@ -154,7 +149,7 @@ extension FollowerListViewModel.State {
let response = try await viewModel.context.apiService.followers(
userID: userID,
maxID: maxID,
authenticationBox: authenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch \(response.value.count) followers")

View File

@ -20,11 +20,13 @@ final class FollowerListViewModel {
// input
let context: AppContext
let domain: CurrentValueSubject<String?, Never>
let userID: CurrentValueSubject<String?, Never>
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var domain: String?
@Published var userID: String?
// output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
private(set) lazy var stateMachine: GKStateMachine = {
@ -40,16 +42,21 @@ final class FollowerListViewModel {
return stateMachine
}()
init(context: AppContext, domain: String?, userID: String?) {
init(
context: AppContext,
authContext: AuthContext,
domain: String?,
userID: String?
) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: domain,
additionalPredicate: nil
)
self.domain = CurrentValueSubject(domain)
self.userID = CurrentValueSubject(userID)
// super.init()
self.domain = domain
self.userID = userID
// end init
}
}

View File

@ -82,8 +82,8 @@ extension FollowingListViewController {
// trigger user timeline loading
Publishers.CombineLatest(
viewModel.domain.removeDuplicates().eraseToAnyPublisher(),
viewModel.userID.removeDuplicates().eraseToAnyPublisher()
viewModel.$domain.removeDuplicates(),
viewModel.$userID.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
@ -101,6 +101,11 @@ extension FollowingListViewController {
}
// MARK: - AuthContextProvider
extension FollowingListViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension FollowingListViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:FollowingListViewController.AutoGenerateTableViewDelegate

View File

@ -7,6 +7,7 @@
import UIKit
import MastodonAsset
import MastodonCore
import MastodonLocalization
extension FollowingListViewModel {
@ -50,10 +51,10 @@ extension FollowingListViewModel {
case is State.Idle, is State.Loading, is State.Fail:
snapshot.appendItems([.bottomLoader], toSection: .main)
case is State.NoMore:
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value,
let userID = self.userID.value,
userID != activeMastodonAuthenticationBox.userID
guard let userID = self.userID,
userID != self.authContext.mastodonAuthenticationBox.userID
else { break }
// display footer exclude self
let text = L10n.Scene.Following.footer
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
default:

View File

@ -50,7 +50,7 @@ extension FollowingListViewModel.State {
guard let viewModel = viewModel else { return false }
switch stateClass {
case is Reloading.Type:
return viewModel.userID.value != nil
return viewModel.userID != nil
default:
return false
}
@ -138,12 +138,7 @@ extension FollowingListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let userID = viewModel.userID.value, !userID.isEmpty else {
stateMachine.enter(Fail.self)
return
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
guard let userID = viewModel.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self)
return
}
@ -153,7 +148,7 @@ extension FollowingListViewModel.State {
let response = try await viewModel.context.apiService.following(
userID: userID,
maxID: maxID,
authenticationBox: authenticationBox
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch \(response.value.count)")

View File

@ -19,11 +19,13 @@ final class FollowingListViewModel {
// input
let context: AppContext
let domain: CurrentValueSubject<String?, Never>
let userID: CurrentValueSubject<String?, Never>
let authContext: AuthContext
let userFetchedResultsController: UserFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var domain: String?
@Published var userID: String?
// output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
private(set) lazy var stateMachine: GKStateMachine = {
@ -39,15 +41,21 @@ final class FollowingListViewModel {
return stateMachine
}()
init(context: AppContext, domain: String?, userID: String?) {
init(
context: AppContext,
authContext: AuthContext,
domain: String?,
userID: String?
) {
self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext,
domain: domain,
additionalPredicate: nil
)
self.domain = CurrentValueSubject(domain)
self.userID = CurrentValueSubject(userID)
self.domain = domain
self.userID = userID
// super.init()
}

View File

@ -332,10 +332,11 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
else { return }
let followerListViewModel = FollowerListViewModel(
context: context,
authContext: viewModel.authContext,
domain: domain,
userID: userID
)
coordinator.present(
_ = coordinator.present(
scene: .follower(viewModel: followerListViewModel),
from: self,
transition: .show
@ -346,10 +347,11 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
else { return }
let followingListViewModel = FollowingListViewModel(
context: context,
authContext: viewModel.authContext,
domain: domain,
userID: userID
)
coordinator.present(
_ = coordinator.present(
scene: .following(viewModel: followingListViewModel),
from: self,
transition: .show

View File

@ -24,6 +24,8 @@ final class ProfileHeaderViewModel {
// input
let context: AppContext
let authContext: AuthContext
@Published var user: MastodonUser?
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
@ -41,8 +43,9 @@ final class ProfileHeaderViewModel {
@Published var isTitleViewDisplaying = false
@Published var isTitleViewContentOffsetSet = false
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
$accountForEdit
.receive(on: DispatchQueue.main)

View File

@ -15,10 +15,12 @@ import MastodonSDK
final class MeProfileViewModel: ProfileViewModel {
init(context: AppContext) {
init(context: AppContext, authContext: AuthContext) {
let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
super.init(
context: context,
optionalMastodonUser: context.authenticationService.activeMastodonAuthentication.value?.user
authContext: authContext,
optionalMastodonUser: user
)
$me

View File

@ -111,7 +111,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
let viewController = ProfileHeaderViewController()
viewController.context = context
viewController.coordinator = coordinator
viewController.viewModel = ProfileHeaderViewModel(context: context)
viewController.viewModel = ProfileHeaderViewModel(context: context, authContext: viewModel.authContext)
return viewController
}()
@ -460,14 +460,14 @@ extension ProfileViewController {
switch meta {
case .url(_, _, let url, _):
guard let url = URL(string: url) else { return }
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
_ = coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
case .mention(_, _, let userInfo):
guard let href = userInfo?["href"] as? String,
let url = URL(string: href) else { return }
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
_ = coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
case .hashtag(_, let hashtag, _):
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag)
coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: viewModel.authContext, hashtag: hashtag)
_ = coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show)
case .email, .emoji:
break
}
@ -485,7 +485,7 @@ extension ProfileViewController {
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, setting: setting)
let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting)
coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
@ -513,24 +513,23 @@ extension ProfileViewController {
@objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let favoriteViewModel = FavoriteViewModel(context: context)
let favoriteViewModel = FavoriteViewModel(context: context, authContext: viewModel.authContext)
coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show)
}
@objc private func bookmarkBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let bookmarkViewModel = BookmarkViewModel(context: context)
let bookmarkViewModel = BookmarkViewModel(context: context, authContext: viewModel.authContext)
coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show)
}
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let mastodonUser = viewModel.user else { return }
let composeViewModel = ComposeViewModel(
context: context,
composeKind: .mention(user: .init(objectID: mastodonUser.objectID)),
authenticationBox: authenticationBox
authContext: viewModel.authContext
)
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
@ -671,6 +670,11 @@ extension ProfileViewController: TabBarPagerDataSource {
//
//}
// MARK: - AuthContextProvider
extension ProfileViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - ProfileHeaderViewControllerDelegate
extension ProfileViewController: ProfileHeaderViewControllerDelegate {
func profileHeaderViewController(
@ -760,16 +764,13 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
case .follow, .request, .pending, .following:
guard let user = viewModel.user else { return }
let reocrd = ManagedObjectRecord<MastodonUser>(objectID: user.objectID)
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
Task {
try await DataSourceFacade.responseToUserFollowAction(
dependency: self,
user: reocrd,
authenticationBox: authenticationBox
user: reocrd
)
}
case .muting:
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let user = viewModel.user else { return }
let name = user.displayNameWithFallback
@ -784,8 +785,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
Task {
try await DataSourceFacade.responseToUserMuteAction(
dependency: self,
user: record,
authenticationBox: authenticationBox
user: record
)
}
}
@ -794,7 +794,6 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
case .blocking:
guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let user = viewModel.user else { return }
let name = user.displayNameWithFallback
@ -809,8 +808,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate {
Task {
try await DataSourceFacade.responseToUserBlockAction(
dependency: self,
user: record,
authenticationBox: authenticationBox
user: record
)
}
}
@ -852,7 +850,6 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate {
// MARK: - MastodonMenuDelegate
extension ProfileViewController: MastodonMenuDelegate {
func menuAction(_ action: MastodonMenu.Action) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let user = viewModel.user else { return }
let userRecord: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
@ -866,8 +863,7 @@ extension ProfileViewController: MastodonMenuDelegate {
status: nil,
button: nil,
barButtonItem: self.moreMenuBarButtonItem
),
authenticationBox: authenticationBox
)
)
} // end Task
}

View File

@ -35,6 +35,7 @@ class ProfileViewModel: NSObject {
// input
let context: AppContext
let authContext: AuthContext
@Published var me: MastodonUser?
@Published var user: MastodonUser?
@ -58,21 +59,25 @@ class ProfileViewModel: NSObject {
// @Published var protected: Bool? = nil
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) {
self.context = context
self.authContext = authContext
self.user = mastodonUser
self.postsUserTimelineViewModel = UserTimelineViewModel(
context: context,
authContext: authContext,
title: L10n.Scene.Profile.SegmentedControl.posts,
queryFilter: .init(excludeReplies: true)
)
self.repliesUserTimelineViewModel = UserTimelineViewModel(
context: context,
authContext: authContext,
title: L10n.Scene.Profile.SegmentedControl.postsAndReplies,
queryFilter: .init(excludeReplies: false)
)
self.mediaUserTimelineViewModel = UserTimelineViewModel(
context: context,
authContext: authContext,
title: L10n.Scene.Profile.SegmentedControl.media,
queryFilter: .init(onlyMedia: true)
)
@ -80,13 +85,7 @@ class ProfileViewModel: NSObject {
super.init()
// bind me
context.authenticationService.activeMastodonAuthenticationBox
.receive(on: DispatchQueue.main)
.sink { [weak self] authenticationBox in
guard let self = self else { return }
self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user
}
.store(in: &disposeBag)
self.me = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
$me
.assign(to: \.me, on: relationshipViewModel)
.store(in: &disposeBag)
@ -132,21 +131,18 @@ class ProfileViewModel: NSObject {
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
// observe friendship
Publishers.CombineLatest3(
Publishers.CombineLatest(
userRecord,
context.authenticationService.activeMastodonAuthenticationBox,
pendingRetryPublisher
)
.sink { [weak self] userRecord, authenticationBox, _ in
.sink { [weak self] userRecord, _ in
guard let self = self else { return }
guard let userRecord = userRecord,
let authenticationBox = authenticationBox
else { return }
guard let userRecord = userRecord else { return }
Task {
do {
let response = try await self.updateRelationship(
record: userRecord,
authenticationBox: authenticationBox
authenticationBox: self.authContext.mastodonAuthenticationBox
)
// there are seconds delay after request follow before requested -> following. Query again when needs
guard let relationship = response.value.first else { return }
@ -216,10 +212,7 @@ extension ProfileViewModel {
headerProfileInfo: ProfileHeaderViewModel.ProfileInfo,
aboutProfileInfo: ProfileAboutViewModel.ProfileInfo
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Account> {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
throw APIService.APIError.implicit(.badRequest)
}
let authenticationBox = authContext.mastodonAuthenticationBox
let domain = authenticationBox.domain
let authorization = authenticationBox.userAuthorization

View File

@ -14,14 +14,11 @@ import MastodonCore
final class RemoteProfileViewModel: ProfileViewModel {
init(context: AppContext, userID: Mastodon.Entity.Account.ID) {
super.init(context: context, optionalMastodonUser: nil)
init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) {
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
let domain = activeMastodonAuthenticationBox.domain
let authorization = activeMastodonAuthenticationBox.userAuthorization
let domain = authContext.mastodonAuthenticationBox.domain
let authorization = authContext.mastodonAuthenticationBox.userAuthorization
Just(userID)
.asyncMap { userID in
try await context.apiService.accountInfo(
@ -54,23 +51,19 @@ final class RemoteProfileViewModel: ProfileViewModel {
.store(in: &disposeBag)
}
init(context: AppContext, notificationID: Mastodon.Entity.Notification.ID) {
super.init(context: context, optionalMastodonUser: nil)
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
return
}
init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) {
super.init(context: context, authContext: authContext, optionalMastodonUser: nil)
Task { @MainActor in
let response = try await context.apiService.notification(
notificationID: notificationID,
authenticationBox: authenticationBox
authenticationBox: authContext.mastodonAuthenticationBox
)
let userID = response.value.account.id
let _user: MastodonUser? = try await context.managedObjectContext.perform {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: authenticationBox.domain, id: userID)
request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID)
request.fetchLimit = 1
return context.managedObjectContext.safeFetch(request).first
}
@ -79,14 +72,14 @@ final class RemoteProfileViewModel: ProfileViewModel {
self.user = user
} else {
_ = try await context.apiService.accountInfo(
domain: authenticationBox.domain,
domain: authContext.mastodonAuthenticationBox.domain,
userID: userID,
authorization: authenticationBox.userAuthorization
authorization: authContext.mastodonAuthenticationBox.userAuthorization
)
let _user: MastodonUser? = try await context.managedObjectContext.perform {
let request = MastodonUser.sortedFetchRequest
request.predicate = MastodonUser.predicate(domain: authenticationBox.domain, id: userID)
request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID)
request.fetchLimit = 1
return context.managedObjectContext.safeFetch(request).first
}

View File

@ -103,6 +103,11 @@ extension UserTimelineViewController: CellFrameCacheContainer {
}
}
// MARK: - AuthContextProvider
extension UserTimelineViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate
extension UserTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:UserTimelineViewController.AutoGenerateTableViewDelegate

View File

@ -18,6 +18,7 @@ extension UserTimelineViewModel {
tableView: tableView,
context: context,
configuration: StatusSection.Configuration(
authContext: authContext,
statusTableViewCellDelegate: statusTableViewCellDelegate,
timelineMiddleLoaderTableViewCellDelegate: nil,
filterContext: .none,

Some files were not shown because too many files have changed in this diff Show More