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 \ xcodebuild -workspace Mastodon.xcworkspace \
-scheme Mastodon \ -scheme Mastodon \
-destination "platform=iOS Simulator,name=iPhone SE (2nd generation)" \
clean \ clean \
build | xcpretty build | xcpretty

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ final public class SceneCoordinator {
private weak var sceneDelegate: SceneDelegate! private weak var sceneDelegate: SceneDelegate!
private weak var appContext: AppContext! private weak var appContext: AppContext!
private var authContext: AuthContext? private(set) var authContext: AuthContext?
let id = UUID().uuidString let id = UUID().uuidString
@ -45,100 +45,83 @@ final public class SceneCoordinator {
appContext.notificationService.requestRevealNotificationPublisher appContext.notificationService.requestRevealNotificationPublisher
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.compactMap { [weak self] pushNotification -> AnyPublisher<MastodonPushNotification?, Never> in .sink(receiveValue: { [weak self] pushNotification 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()
}
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()
} else {
// switch to notification's account
let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
request.returnsObjectsAsFaults = false
request.fetchLimit = 1
do {
guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
return Just(nil).eraseToAnyPublisher()
}
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
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 self = self else { return }
guard let pushNotification = pushNotification else { return } Task {
guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return }
// redirect to notification tab let accessToken = pushNotification.accessToken // use raw accessToken value without normalize
self.switchToTabBar(tab: .notification) if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken {
// do nothing if notification for current account
return
// Delay in next run loop } else {
DispatchQueue.main.async { [weak self] in // switch to notification's account
guard let self = self else { return } let request = MastodonAuthentication.sortedFetchRequest
request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
// Note: request.returnsObjectsAsFaults = false
// show (push) on phone and pad request.fetchLimit = 1
let from: UIViewController? = { do {
if let splitViewController = self.splitViewController { guard let authentication = try appContext.managedObjectContext.fetch(request).first else {
if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil { return
// compact
return splitViewController.compactMainTabBarViewController.topMost
} else {
// expand
return splitViewController.contentSplitViewController.mainTabBarController.topMost
} }
} else { let domain = authentication.domain
return self.tabBarController.topMost let userID = authentication.userID
let isSuccess = try await appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID)
guard isSuccess else { return }
self.setup()
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 }
// Note:
// show (push) on phone and pad
let from: UIViewController? = {
if let splitViewController = self.splitViewController {
if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil {
// compact
return splitViewController.compactMainTabBarViewController.topMost
} else {
// expand
return splitViewController.contentSplitViewController.mainTabBarController.topMost
}
} else {
return self.tabBarController.topMost
}
}()
// 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, 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, 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
} }
}()
// show notification related content
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) 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)
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)
case ._other:
assertionFailure()
break
} }
} // end DispatchQueue.main.async } // end Task
} })
.store(in: &disposeBag) .store(in: &disposeBag)
} }
} }
@ -180,7 +163,7 @@ extension SceneCoordinator {
case hashtagTimeline(viewModel: HashtagTimelineViewModel) case hashtagTimeline(viewModel: HashtagTimelineViewModel)
// profile // profile
case accountList case accountList(viewModel: AccountListViewModel)
case profile(viewModel: ProfileViewModel) case profile(viewModel: ProfileViewModel)
case favorite(viewModel: FavoriteViewModel) case favorite(viewModel: FavoriteViewModel)
case follower(viewModel: FollowerListViewModel) case follower(viewModel: FollowerListViewModel)
@ -260,6 +243,19 @@ extension SceneCoordinator {
transition: .modal(animated: true, completion: nil) 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 { } catch {
@ -431,8 +427,9 @@ private extension SceneCoordinator {
let _viewController = HashtagTimelineViewController() let _viewController = HashtagTimelineViewController()
_viewController.viewModel = viewModel _viewController.viewModel = viewModel
viewController = _viewController viewController = _viewController
case .accountList: case .accountList(let viewModel):
let _viewController = AccountListViewController() let _viewController = AccountListViewController()
_viewController.viewModel = viewModel
viewController = _viewController viewController = _viewController
case .profile(let viewModel): case .profile(let viewModel):
let _viewController = ProfileViewController() let _viewController = ProfileViewController()

View File

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

View File

@ -23,13 +23,16 @@ extension DiscoverySection {
static let logger = Logger(subsystem: "DiscoverySection", category: "logic") static let logger = Logger(subsystem: "DiscoverySection", category: "logic")
class Configuration { class Configuration {
let authContext: AuthContext
weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate?
let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher?
public init( public init(
authContext: AuthContext,
profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil, profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil,
familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil
) { ) {
self.authContext = authContext
self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate
self.familiarFollowers = familiarFollowers self.familiarFollowers = familiarFollowers
} }
@ -73,11 +76,9 @@ extension DiscoverySection {
} else { } else {
cell.profileCardView.viewModel.familiarFollowers = nil 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 return cell
case .bottomLoader: case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell 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 { extension NotificationSection {
struct Configuration { struct Configuration {
let authContext: AuthContext
weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate? weak var notificationTableViewCellDelegate: NotificationTableViewCellDelegate?
let filterContext: Mastodon.Entity.Filter.Context? let filterContext: Mastodon.Entity.Filter.Context?
let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher? let activeFilters: Published<[Mastodon.Entity.Filter]>.Publisher?
@ -74,21 +75,20 @@ extension NotificationSection {
viewModel: NotificationTableViewCell.ViewModel, viewModel: NotificationTableViewCell.ViewModel,
configuration: Configuration configuration: Configuration
) { ) {
cell.notificationView.viewModel.authContext = configuration.authContext
StatusSection.setupStatusPollDataSource( StatusSection.setupStatusPollDataSource(
context: context, context: context,
authContext: configuration.authContext,
statusView: cell.notificationView.statusView statusView: cell.notificationView.statusView
) )
StatusSection.setupStatusPollDataSource( StatusSection.setupStatusPollDataSource(
context: context, context: context,
authContext: configuration.authContext,
statusView: cell.notificationView.quoteStatusView statusView: cell.notificationView.quoteStatusView
) )
context.authenticationService.activeMastodonAuthenticationBox
.map { $0 as UserIdentifier? }
.assign(to: \.userIdentifier, on: cell.notificationView.viewModel)
.store(in: &cell.disposeBag)
cell.configure( cell.configure(
tableView: tableView, tableView: tableView,
viewModel: viewModel, viewModel: viewModel,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import MastodonCore
import MastodonUI import MastodonUI
import MastodonLocalization import MastodonLocalization
extension UITableViewDelegate where Self: DataSourceProvider { extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider {
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 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)") 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. // Created by Cirno MainasuK on 2021-9-13.
// //
import os.log
import UIKit import UIKit
import Combine import Combine
import CoreData import CoreData
@ -14,43 +15,43 @@ import MastodonMeta
import MastodonCore import MastodonCore
import MastodonUI import MastodonUI
final class AccountListViewModel { final class AccountListViewModel: NSObject {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
// output // output
let authentications = CurrentValueSubject<[Item], Never>([]) @Published var authentications: [ManagedObjectRecord<MastodonAuthentication>] = []
let activeMastodonUserObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil) @Published var items: [Item] = []
let dataSourceDidUpdate = PassthroughSubject<Void, Never>() let dataSourceDidUpdate = PassthroughSubject<Void, Never>()
var diffableDataSource: UITableViewDiffableDataSource<Section, Item>! var diffableDataSource: UITableViewDiffableDataSource<Section, Item>!
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
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
)
return controller
}()
super.init()
// end init
mastodonAuthenticationFetchedResultsController.delegate = self
Publishers.CombineLatest( $authentications
context.authenticationService.mastodonAuthentications,
context.authenticationService.activeMastodonAuthentication
)
.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)
authentications
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] authentications in .sink { [weak self] authentications in
guard let self = self else { return } guard let self = self else { return }
@ -58,7 +59,10 @@ final class AccountListViewModel {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main]) 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) snapshot.appendItems([.addAccount], toSection: .main)
diffableDataSource.apply(snapshot) { diffableDataSource.apply(snapshot) {
@ -76,7 +80,7 @@ extension AccountListViewModel {
} }
enum Item: Hashable { enum Item: Hashable {
case authentication(objectID: NSManagedObjectID) case authentication(record: ManagedObjectRecord<MastodonAuthentication>)
case addAccount case addAccount
} }
@ -86,14 +90,17 @@ extension AccountListViewModel {
) { ) {
diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
switch item { switch item {
case .authentication(let objectID): case .authentication(let record):
let authentication = managedObjectContext.object(with: objectID) as! MastodonAuthentication
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell
AccountListViewModel.configure( if let authentication = record.object(in: managedObjectContext),
cell: cell, let activeAuthentication = self.authContext.mastodonAuthenticationBox.authenticationRecord.object(in: managedObjectContext)
authentication: authentication, {
activeMastodonUserObjectID: self.activeMastodonUserObjectID.eraseToAnyPublisher() AccountListViewModel.configure(
) cell: cell,
authentication: authentication,
activeAuthentication: activeAuthentication
)
}
return cell return cell
case .addAccount: case .addAccount:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AddAccountTableViewCell.self), for: indexPath) as! AddAccountTableViewCell
@ -109,7 +116,7 @@ extension AccountListViewModel {
static func configure( static func configure(
cell: AccountListTableViewCell, cell: AccountListTableViewCell,
authentication: MastodonAuthentication, authentication: MastodonAuthentication,
activeMastodonUserObjectID: AnyPublisher<NSManagedObjectID?, Never> activeAuthentication: MastodonAuthentication
) { ) {
let user = authentication.user let user = authentication.user
@ -138,19 +145,14 @@ extension AccountListViewModel {
cell.badgeButton.setBadge(number: count) cell.badgeButton.setBadge(number: count)
// checkmark // checkmark
activeMastodonUserObjectID let isActive = activeAuthentication.userID == authentication.userID
.receive(on: DispatchQueue.main) cell.tintColor = .label
.sink { objectID in cell.checkmarkImageView.isHidden = !isActive
let isCurrentUser = user.objectID == objectID if isActive {
cell.tintColor = .label cell.accessibilityTraits.insert(.selected)
cell.checkmarkImageView.isHidden = !isCurrentUser } else {
if isCurrentUser { cell.accessibilityTraits.remove(.selected)
cell.accessibilityTraits.insert(.selected) }
} else {
cell.accessibilityTraits.remove(.selected)
}
}
.store(in: &cell.disposeBag)
cell.accessibilityLabel = [ cell.accessibilityLabel = [
cell.nameLabel.text, cell.nameLabel.text,
@ -161,3 +163,21 @@ extension AccountListViewModel {
.joined(separator: " ") .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) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
private(set) lazy var viewModel = AccountListViewModel(context: context) var viewModel: AccountListViewModel!
private(set) lazy var addBarButtonItem: UIBarButtonItem = { private(set) lazy var addBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem( let barButtonItem = UIBarButtonItem(
@ -64,7 +64,10 @@ extension AccountListViewController: PanModalPresentable {
return .contentHeight(CGFloat(height)) 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) let height = calculateHeight(of: count)
return .contentHeight(height) return .contentHeight(height)
} }
@ -174,16 +177,14 @@ extension AccountListViewController: UITableViewDelegate {
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item { switch item {
case .authentication(let objectID): case .authentication(let record):
assert(Thread.isMainThread) assert(Thread.isMainThread)
let authentication = context.managedObjectContext.object(with: objectID) as! MastodonAuthentication guard let authentication = record.object(in: context.managedObjectContext) else { return }
context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) Task { @MainActor in
.receive(on: DispatchQueue.main) let isActive = try await context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
.sink { [weak self] result in guard isActive else { return }
guard let self = self else { return } self.coordinator.setup()
self.coordinator.setup() } // end Task
}
.store(in: &disposeBag)
case .addAccount: case .addAccount:
// TODO: add dismiss entry for welcome scene // TODO: add dismiss entry for welcome scene
coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,41 +22,37 @@ final class DiscoveryHashtagsViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext
let viewDidAppeared = PassthroughSubject<Void, Never>() let viewDidAppeared = PassthroughSubject<Void, Never>()
// output // output
var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>? var diffableDataSource: UITableViewDiffableDataSource<DiscoverySection, DiscoveryItem>?
@Published var hashtags: [Mastodon.Entity.Tag] = [] @Published var hashtags: [Mastodon.Entity.Tag] = []
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext
// end init // end init
Publishers.CombineLatest( viewDidAppeared
context.authenticationService.activeMastodonAuthenticationBox, .throttle(for: 3, scheduler: DispatchQueue.main, latest: true)
viewDidAppeared .asyncMap { authenticationBox in
) try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil)
.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)
}
.retry(3)
.map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
.catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { throw error }) }
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
self.hashtags = response.value.filter { !$0.name.isEmpty }
case .failure:
break
} }
} .retry(3)
.store(in: &disposeBag) .map { response in Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { response } }
.catch { error in Just(Result<Mastodon.Response.Content<[Mastodon.Entity.Tag]>, Error> { throw error }) }
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
self.hashtags = response.value.filter { !$0.name.isEmpty }
case .failure:
break
}
}
.store(in: &disposeBag)
} }
deinit { deinit {
@ -69,8 +65,7 @@ extension DiscoveryHashtagsViewModel {
@MainActor @MainActor
func fetch() async throws { func fetch() async throws {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } let response = try await context.apiService.trendHashtags(domain: authContext.mastodonAuthenticationBox.domain, query: nil)
let response = try await context.apiService.trendHashtags(domain: authenticationBox.domain, query: nil)
hashtags = response.value.filter { !$0.name.isEmpty } hashtags = response.value.filter { !$0.name.isEmpty }
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch tags: \(response.value.count)") logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch tags: \(response.value.count)")
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ final class DiscoveryPostsViewModel {
// input // input
let context: AppContext let context: AppContext
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -41,20 +42,16 @@ final class DiscoveryPostsViewModel {
let didLoadLatest = PassthroughSubject<Void, Never>() let didLoadLatest = PassthroughSubject<Void, Never>()
@Published var isServerSupportEndpoint = true @Published var isServerSupportEndpoint = true
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController( self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: nil, domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil additionalTweetPredicate: nil
) )
// end init // end init
context.authenticationService.activeMastodonAuthentication
.map { $0?.domain }
.assign(to: \.domain, on: statusFetchedResultsController)
.store(in: &disposeBag)
Task { Task {
await checkServerEndpoint() await checkServerEndpoint()
} // end Task } // end Task
@ -68,11 +65,9 @@ final class DiscoveryPostsViewModel {
extension DiscoveryPostsViewModel { extension DiscoveryPostsViewModel {
func checkServerEndpoint() async { func checkServerEndpoint() async {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do { do {
_ = try await context.apiService.trendStatuses( _ = try await context.apiService.trendStatuses(
domain: authenticationBox.domain, domain: authContext.mastodonAuthenticationBox.domain,
query: .init(offset: nil, limit: nil) query: .init(offset: nil, limit: nil)
) )
} catch let error as Mastodon.API.Error where error.httpResponseStatus.code == 404 { } 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) { @objc private func composeBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) 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( let composeViewModel = ComposeViewModel(
context: context, context: context,
composeKind: .hashtag(hashtag: viewModel.hashtag), composeKind: .hashtag(hashtag: viewModel.hashtag),
authenticationBox: authenticationBox authContext: viewModel.authContext
) )
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) 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 // MARK: - UITableViewDelegate
extension HashtagTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { extension HashtagTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:HashtagTimelineViewController.AutoGenerateTableViewDelegate // sourcery:inline:HashtagTimelineViewController.AutoGenerateTableViewDelegate

View File

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

View File

@ -135,12 +135,6 @@ extension HashtagTimelineViewModel.State {
break 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 // TODO: only set large count when using Wi-Fi
let maxID = self.maxID let maxID = self.maxID
let isReloading = maxID == nil let isReloading = maxID == nil
@ -148,10 +142,10 @@ extension HashtagTimelineViewModel.State {
Task { Task {
do { do {
let response = try await viewModel.context.apiService.hashtagTimeline( let response = try await viewModel.context.apiService.hashtagTimeline(
domain: authenticationBox.domain, domain: viewModel.authContext.mastodonAuthenticationBox.domain,
maxID: maxID, maxID: maxID,
hashtag: viewModel.hashtag, hashtag: viewModel.hashtag,
authenticationBox: authenticationBox authenticationBox: viewModel.authContext.mastodonAuthenticationBox
) )
let newMaxID: String? = { let newMaxID: String? = {

View File

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

View File

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

View File

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

View File

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

View File

@ -63,11 +63,6 @@ extension HomeTimelineViewModel.LoadLatestState {
override func didEnter(from previousState: GKState?) { override func didEnter(from previousState: GKState?) {
super.didEnter(from: previousState) super.didEnter(from: previousState)
guard let viewModel = viewModel, let stateMachine = stateMachine else { return } 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 latestFeedRecords = viewModel.fetchedResultsController.records.prefix(APIService.onceRequestStatusMaxCount)
let parentManagedObjectContext = viewModel.fetchedResultsController.fetchedResultsController.managedObjectContext let parentManagedObjectContext = viewModel.fetchedResultsController.fetchedResultsController.managedObjectContext
@ -85,7 +80,7 @@ extension HomeTimelineViewModel.LoadLatestState {
do { do {
let response = try await viewModel.context.apiService.homeTimeline( let response = try await viewModel.context.apiService.homeTimeline(
authenticationBox: activeMastodonAuthenticationBox authenticationBox: viewModel.authContext.mastodonAuthenticationBox
) )
await enter(state: Idle.self) await enter(state: Idle.self)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,8 @@ final class BookmarkViewModel {
// input // input
let context: AppContext let context: AppContext
let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never> let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -37,23 +38,14 @@ final class BookmarkViewModel {
return stateMachine return stateMachine
}() }()
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController( self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: nil, domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil 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 { final class CachedProfileViewModel: ProfileViewModel {
init(context: AppContext, mastodonUser: MastodonUser) { init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUser) {
super.init(context: context, optionalMastodonUser: 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)") 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 // MARK: - UITableViewDelegate
extension FamiliarFollowersViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { extension FamiliarFollowersViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:FamiliarFollowersViewController.AutoGenerateTableViewDelegate // sourcery:inline:FamiliarFollowersViewController.AutoGenerateTableViewDelegate

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ final class FavoriteViewModel {
// input // input
let context: AppContext let context: AppContext
let activeMastodonAuthenticationBox: CurrentValueSubject<MastodonAuthenticationBox?, Never> let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -37,23 +37,14 @@ final class FavoriteViewModel {
return stateMachine return stateMachine
}() }()
init(context: AppContext) { init(context: AppContext, authContext: AuthContext) {
self.context = context self.context = context
self.activeMastodonAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController( self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: nil, domain: authContext.mastodonAuthenticationBox.domain,
additionalTweetPredicate: nil 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,15 +82,15 @@ extension FollowerListViewController {
// trigger user timeline loading // trigger user timeline loading
Publishers.CombineLatest( Publishers.CombineLatest(
viewModel.domain.removeDuplicates().eraseToAnyPublisher(), viewModel.$domain.removeDuplicates(),
viewModel.userID.removeDuplicates().eraseToAnyPublisher() viewModel.$userID.removeDuplicates()
) )
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self) self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self)
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@ -101,6 +101,12 @@ extension FollowerListViewController {
} }
// MARK: - AuthContextProvider
extension FollowerListViewController: AuthContextProvider {
var authContext: AuthContext { viewModel.authContext }
}
// MARK: - UITableViewDelegate // MARK: - UITableViewDelegate
extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableViewDelegate {
// sourcery:inline:FollowerListViewController.AutoGenerateTableViewDelegate // sourcery:inline:FollowerListViewController.AutoGenerateTableViewDelegate

View File

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

View File

@ -51,7 +51,7 @@ extension FollowerListViewModel.State {
guard let viewModel = viewModel else { return false } guard let viewModel = viewModel else { return false }
switch stateClass { switch stateClass {
case is Reloading.Type: case is Reloading.Type:
return viewModel.userID.value != nil return viewModel.userID != nil
default: default:
return false return false
} }
@ -139,12 +139,7 @@ extension FollowerListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return } guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let userID = viewModel.userID.value, !userID.isEmpty else { guard let userID = viewModel.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self)
return
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self) stateMachine.enter(Fail.self)
return return
} }
@ -154,7 +149,7 @@ extension FollowerListViewModel.State {
let response = try await viewModel.context.apiService.followers( let response = try await viewModel.context.apiService.followers(
userID: userID, userID: userID,
maxID: maxID, 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") 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 // input
let context: AppContext let context: AppContext
let domain: CurrentValueSubject<String?, Never> let authContext: AuthContext
let userID: CurrentValueSubject<String?, Never>
let userFetchedResultsController: UserFetchedResultsController let userFetchedResultsController: UserFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var domain: String?
@Published var userID: String?
// output // output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>? var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
private(set) lazy var stateMachine: GKStateMachine = { private(set) lazy var stateMachine: GKStateMachine = {
@ -40,16 +42,21 @@ final class FollowerListViewModel {
return stateMachine return stateMachine
}() }()
init(context: AppContext, domain: String?, userID: String?) { init(
context: AppContext,
authContext: AuthContext,
domain: String?,
userID: String?
) {
self.context = context self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController( self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: domain, domain: domain,
additionalPredicate: nil additionalPredicate: nil
) )
self.domain = CurrentValueSubject(domain) self.domain = domain
self.userID = CurrentValueSubject(userID) self.userID = userID
// super.init() // end init
} }
} }

View File

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

View File

@ -7,6 +7,7 @@
import UIKit import UIKit
import MastodonAsset import MastodonAsset
import MastodonCore
import MastodonLocalization import MastodonLocalization
extension FollowingListViewModel { extension FollowingListViewModel {
@ -44,23 +45,23 @@ extension FollowingListViewModel {
snapshot.appendSections([.main]) snapshot.appendSections([.main])
let items = records.map { UserItem.user(record: $0) } let items = records.map { UserItem.user(record: $0) }
snapshot.appendItems(items, toSection: .main) snapshot.appendItems(items, toSection: .main)
if let currentState = self.stateMachine.currentState { if let currentState = self.stateMachine.currentState {
switch currentState { switch currentState {
case is State.Idle, is State.Loading, is State.Fail: case is State.Idle, is State.Loading, is State.Fail:
snapshot.appendItems([.bottomLoader], toSection: .main) snapshot.appendItems([.bottomLoader], toSection: .main)
case is State.NoMore: case is State.NoMore:
guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value, guard let userID = self.userID,
let userID = self.userID.value, userID != self.authContext.mastodonAuthenticationBox.userID
userID != activeMastodonAuthenticationBox.userID
else { break } else { break }
// display footer exclude self
let text = L10n.Scene.Following.footer let text = L10n.Scene.Following.footer
snapshot.appendItems([.bottomHeader(text: text)], toSection: .main) snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
default: default:
break break
} }
} }
diffableDataSource.apply(snapshot, animatingDifferences: false) diffableDataSource.apply(snapshot, animatingDifferences: false)
} }
.store(in: &disposeBag) .store(in: &disposeBag)

View File

@ -50,7 +50,7 @@ extension FollowingListViewModel.State {
guard let viewModel = viewModel else { return false } guard let viewModel = viewModel else { return false }
switch stateClass { switch stateClass {
case is Reloading.Type: case is Reloading.Type:
return viewModel.userID.value != nil return viewModel.userID != nil
default: default:
return false return false
} }
@ -138,12 +138,7 @@ extension FollowingListViewModel.State {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return } guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let userID = viewModel.userID.value, !userID.isEmpty else { guard let userID = viewModel.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self)
return
}
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
stateMachine.enter(Fail.self) stateMachine.enter(Fail.self)
return return
} }
@ -153,7 +148,7 @@ extension FollowingListViewModel.State {
let response = try await viewModel.context.apiService.following( let response = try await viewModel.context.apiService.following(
userID: userID, userID: userID,
maxID: maxID, 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)") 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 // input
let context: AppContext let context: AppContext
let domain: CurrentValueSubject<String?, Never> let authContext: AuthContext
let userID: CurrentValueSubject<String?, Never>
let userFetchedResultsController: UserFetchedResultsController let userFetchedResultsController: UserFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var domain: String?
@Published var userID: String?
// output // output
var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>? var diffableDataSource: UITableViewDiffableDataSource<UserSection, UserItem>?
private(set) lazy var stateMachine: GKStateMachine = { private(set) lazy var stateMachine: GKStateMachine = {
@ -39,15 +41,21 @@ final class FollowingListViewModel {
return stateMachine return stateMachine
}() }()
init(context: AppContext, domain: String?, userID: String?) { init(
context: AppContext,
authContext: AuthContext,
domain: String?,
userID: String?
) {
self.context = context self.context = context
self.authContext = authContext
self.userFetchedResultsController = UserFetchedResultsController( self.userFetchedResultsController = UserFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: domain, domain: domain,
additionalPredicate: nil additionalPredicate: nil
) )
self.domain = CurrentValueSubject(domain) self.domain = domain
self.userID = CurrentValueSubject(userID) self.userID = userID
// super.init() // super.init()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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