From 94c51eaed6503373ee727660a8fc3bb4423cde02 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 19 Oct 2023 13:09:59 +0200 Subject: [PATCH 01/24] Migrate FollowButton to use UIButton.Configuration also: Refactor Button-Background-Stuff --- .../background.colorset/Contents.json | 38 ++++++++ .../background.dark.colorset/Contents.json | 20 ----- .../Contents.json | 38 ++++++++ .../Contents.json | 20 ----- .../Contents.json | 20 ----- .../background.light.colorset/Contents.json | 20 ----- .../MastodonAsset/Generated/Assets.swift | 6 +- .../View/Content/FollowButton.swift | 21 ++--- .../MastodonUI/View/Content/UserView.swift | 89 +++++++++---------- .../ProfileRelationshipActionButton.swift | 13 +-- 10 files changed, 130 insertions(+), 155 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.colorset/Contents.json delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.dark.colorset/Contents.json create mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.colorset/Contents.json delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.dark.colorset/Contents.json delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.light.colorset/Contents.json delete mode 100644 MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.light.colorset/Contents.json diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.colorset/Contents.json new file mode 100644 index 000000000..a36ab82ce --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.216", + "green" : "0.173", + "red" : "0.157" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.933", + "green" : "0.933", + "red" : "0.933" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.dark.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.dark.colorset/Contents.json deleted file mode 100644 index 63600675a..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.dark.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.933", - "green" : "0.933", - "red" : "0.933" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.colorset/Contents.json new file mode 100644 index 000000000..2dfe8b1c4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.106", + "green" : "0.082", + "red" : "0.075" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.729", + "green" : "0.729", + "red" : "0.729" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.dark.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.dark.colorset/Contents.json deleted file mode 100644 index 4e900a602..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.dark.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.729", - "green" : "0.729", - "red" : "0.729" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.light.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.light.colorset/Contents.json deleted file mode 100644 index 6ba0d80b0..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.highlighted.light.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.106", - "green" : "0.082", - "red" : "0.075" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.light.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.light.colorset/Contents.json deleted file mode 100644 index 70d85d5da..000000000 --- a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/RelationshipButton/background.light.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift index be8f204e0..9414c01e9 100644 --- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift +++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift @@ -190,10 +190,8 @@ public enum Asset { public static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray") } public enum RelationshipButton { - public static let backgroundDark = ColorAsset(name: "Scene/Profile/RelationshipButton/background.dark") - public static let backgroundHighlightedDark = ColorAsset(name: "Scene/Profile/RelationshipButton/background.highlighted.dark") - public static let backgroundHighlightedLight = ColorAsset(name: "Scene/Profile/RelationshipButton/background.highlighted.light") - public static let backgroundLight = ColorAsset(name: "Scene/Profile/RelationshipButton/background.light") + public static let background = ColorAsset(name: "Scene/Profile/RelationshipButton/background") + public static let backgroundHighlighted = ColorAsset(name: "Scene/Profile/RelationshipButton/background.highlighted") } } public enum Report { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift index ff82aa3cb..a7d03a4d2 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift @@ -3,29 +3,18 @@ import UIKit import MastodonAsset -public final class FollowButton: RoundedEdgesButton { +public final class FollowButton: UIButton { public init() { super.init(frame: .zero) configureAppearance() } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func configureAppearance() { - setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) - setTitleColor(Asset.Colors.Label.primaryReverse.color.withAlphaComponent(0.5), for: .highlighted) - switch traitCollection.userInterfaceStyle { - case .dark: - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundDark.color), for: .normal) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .highlighted) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .disabled) - default: - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundLight.color), for: .normal) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled) - } + var buttonConfiguration = UIButton.Configuration.filled() + buttonConfiguration.background.cornerRadius = 10 + self.configuration = buttonConfiguration } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index 9221c7f90..a3acc3e9d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -103,7 +103,6 @@ public final class UserView: UIView { private let followButtonWrapper = UIView() private let followButton: FollowButton = { let button = FollowButton() - button.cornerRadius = 10 button.isHidden = true button.translatesAutoresizingMaskIntoConstraints = false button.setContentCompressionResistancePriority(.required, for: .horizontal) @@ -257,57 +256,57 @@ public extension UserView { func setButtonState(_ state: ButtonState) { currentButtonState = state prepareButtonStateLayout(for: state) - + switch state { - case .loading: - followButtonWrapper.isHidden = false - followButton.isHidden = false - followButton.setTitle(nil, for: .normal) - followButton.setBackgroundColor(Asset.Colors.Button.disabled.color, for: .normal) - - case .follow: - followButtonWrapper.isHidden = false - followButton.isHidden = false - followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal) - followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal) - followButton.setTitleColor(.white, for: .normal) + case .loading: + followButtonWrapper.isHidden = false + followButton.isHidden = false + followButton.configuration?.title = nil + followButton.setBackgroundColor(Asset.Colors.Button.disabled.color, for: .normal) - case .request: - followButtonWrapper.isHidden = false - followButton.isHidden = false - followButton.setTitle(L10n.Common.Controls.Friendship.request, for: .normal) - followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal) - followButton.setTitleColor(.white, for: .normal) + case .follow: + followButtonWrapper.isHidden = false + followButton.isHidden = false + followButton.configuration?.title = L10n.Common.Controls.Friendship.follow + followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userFollow.color + followButton.configuration?.baseForegroundColor = .white - case .pending: - followButtonWrapper.isHidden = false - followButton.isHidden = false - followButton.setTitle(L10n.Common.Controls.Friendship.pending, for: .normal) - followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal) - followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal) + case .request: + followButtonWrapper.isHidden = false + followButton.isHidden = false + followButton.configuration?.title = L10n.Common.Controls.Friendship.request + followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userFollow.color + followButton.configuration?.baseForegroundColor = .white - case .unfollow: - followButtonWrapper.isHidden = false - followButton.isHidden = false - followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal) - followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal) - followButton.setTitleColor(Asset.Colors.Button.userFollowingTitle.color, for: .normal) - - case .blocked: - followButtonWrapper.isHidden = false - followButton.isHidden = false - followButton.setTitle(L10n.Common.Controls.Friendship.blocked, for: .normal) - followButton.setBackgroundColor(Asset.Colors.Button.userBlocked.color, for: .normal) - followButton.setTitleColor(.systemRed, for: .normal) + case .pending: + followButtonWrapper.isHidden = false + followButton.isHidden = false + followButton.configuration?.title = L10n.Common.Controls.Friendship.pending + followButton.configuration?.baseForegroundColor = Asset.Colors.Button.userFollowingTitle.color + followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userFollowing.color - case .none: - followButtonWrapper.isHidden = true - followButton.isHidden = true - followButton.setTitle(nil, for: .normal) - followButton.setBackgroundColor(.clear, for: .normal) + case .unfollow: + followButtonWrapper.isHidden = false + followButton.isHidden = false + followButton.configuration?.title = L10n.Common.Controls.Friendship.following + followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userFollowing.color + followButton.configuration?.baseForegroundColor = Asset.Colors.Button.userFollowingTitle.color + + case .blocked: + followButtonWrapper.isHidden = false + followButton.isHidden = false + followButton.configuration?.title = L10n.Common.Controls.Friendship.blocked + followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userBlocked.color + followButton.configuration?.baseForegroundColor = .systemRed + + case .none: + followButtonWrapper.isHidden = true + followButton.isHidden = true + followButton.configuration?.title = nil + followButton.configuration?.baseBackgroundColor = .clear } - + followButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) followButton.titleLabel?.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15)) } diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift index bdf696e74..46ec8b5dd 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift @@ -77,15 +77,8 @@ extension ProfileRelationshipActionButton { private func configureAppearance() { setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) setTitleColor(Asset.Colors.Label.primaryReverse.color.withAlphaComponent(0.5), for: .highlighted) - switch traitCollection.userInterfaceStyle { - case .dark: - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundDark.color), for: .normal) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .highlighted) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedDark.color), for: .disabled) - default: - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundLight.color), for: .normal) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .highlighted) - setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlightedLight.color), for: .disabled) - } + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.background.color), for: .normal) + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlighted.color), for: .highlighted) + setBackgroundImage(.placeholder(color: Asset.Scene.Profile.RelationshipButton.backgroundHighlighted.color), for: .disabled) } } From fb0758eac4acb78ba2a0a6867d6ca82bc55200c2 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 19 Oct 2023 13:18:44 +0200 Subject: [PATCH 02/24] Remove duplicate conformance automatic conformance FTW! --- .../MastodonSDK/Mastodon+Entity+History.swift | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+History.swift diff --git a/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+History.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+History.swift deleted file mode 100644 index b116889b8..000000000 --- a/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+History.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Mastodon+Entity+History.swift -// Mastodon -// -// Created by xiaojian sun on 2021/4/2. -// - -import MastodonSDK - -extension Mastodon.Entity.History: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(uses) - hasher.combine(accounts) - hasher.combine(day) - } - - public static func == (lhs: Mastodon.Entity.History, rhs: Mastodon.Entity.History) -> Bool { - return lhs.uses == rhs.uses && lhs.uses == rhs.uses && lhs.day == rhs.day - } -} From e640befd9e0fd1efa108be2fa56ff0c598984e27 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 19 Oct 2023 13:21:11 +0200 Subject: [PATCH 03/24] Get rid of FollowButton --- .../View/Content/FollowButton.swift | 20 ------------------- .../MastodonUI/View/Content/UserView.swift | 7 +++++-- 2 files changed, 5 insertions(+), 22 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift deleted file mode 100644 index a7d03a4d2..000000000 --- a/MastodonSDK/Sources/MastodonUI/View/Content/FollowButton.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2023 Mastodon gGmbH. All rights reserved. - -import UIKit -import MastodonAsset - -public final class FollowButton: UIButton { - - public init() { - super.init(frame: .zero) - configureAppearance() - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - private func configureAppearance() { - var buttonConfiguration = UIButton.Configuration.filled() - buttonConfiguration.background.cornerRadius = 10 - self.configuration = buttonConfiguration - } -} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index a3acc3e9d..cd07b3004 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -101,8 +101,11 @@ public final class UserView: UIView { }() private let followButtonWrapper = UIView() - private let followButton: FollowButton = { - let button = FollowButton() + private let followButton: UIButton = { + var buttonConfiguration = UIButton.Configuration.filled() + buttonConfiguration.background.cornerRadius = 10 + + let button = UIButton(configuration: buttonConfiguration) button.isHidden = true button.translatesAutoresizingMaskIntoConstraints = false button.setContentCompressionResistancePriority(.required, for: .horizontal) From b92ffe8a46b47ac6984d355606e7b1182f9c345d Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 19 Oct 2023 14:39:07 +0200 Subject: [PATCH 04/24] Duplicate methods to work with Entity.Account This is preparation, but as you know: Proper Preperation and Planning Prevent Piss Poor Performance --- .../Provider/DataSourceFacade+Block.swift | 24 +++- .../Provider/DataSourceFacade+Follow.swift | 17 ++- .../Provider/DataSourceFacade+UserView.swift | 54 +++++++-- .../View/Content/UserView+Configuration.swift | 5 + .../UserTableViewCell+ViewModel.swift | 26 ++++- .../Service/API/APIService+Block.swift | 93 ++++++++++++++- .../Service/API/APIService+Follow.swift | 108 ++++++++++++++++-- .../View/Content/UserView+ViewModel.swift | 2 + .../MastodonUI/View/Content/UserView.swift | 14 ++- 9 files changed, 319 insertions(+), 24 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index 3f8693476..c8f1f9405 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -8,6 +8,7 @@ import UIKit import CoreDataStack import MastodonCore +import MastodonSDK extension DataSourceFacade { static func responseToUserBlockAction( @@ -29,5 +30,26 @@ extension DataSourceFacade { authenticationBox: authBox ) dependency.context.authenticationService.fetchFollowingAndBlockedAsync() - } // end func + } + + static func responseToUserBlockAction( + dependency: NeedsDependency & AuthContextProvider, + user: Mastodon.Entity.Account + ) async throws { + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() + await selectionFeedbackGenerator.selectionChanged() + + let apiService = dependency.context.apiService + let authBox = dependency.authContext.mastodonAuthenticationBox + + _ = try await apiService.toggleBlock( + user: user, + authenticationBox: authBox + ) + + try await dependency.context.apiService.getBlocked( + authenticationBox: authBox + ) + dependency.context.authenticationService.fetchFollowingAndBlockedAsync() + } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 88503ae7b..6fe0005a0 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -25,7 +25,22 @@ extension DataSourceFacade { authenticationBox: dependency.authContext.mastodonAuthenticationBox ) dependency.context.authenticationService.fetchFollowingAndBlockedAsync() - } // end func + } + + static func responseToUserFollowAction( + dependency: NeedsDependency & AuthContextProvider, + user: Mastodon.Entity.Account + ) async throws { + let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() + await selectionFeedbackGenerator.selectionChanged() + + _ = try await dependency.context.apiService.toggleFollow( + user: user, + authenticationBox: dependency.authContext.mastodonAuthenticationBox + ) + dependency.context.authenticationService.fetchFollowingAndBlockedAsync() + } + } extension DataSourceFacade { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift index 8b1a5c84d..a90bdd0a8 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift @@ -64,16 +64,52 @@ extension DataSourceFacade { break //no-op } } -} -extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider { - func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) { - Task { - try await DataSourceFacade.responseToUserViewButtonAction( - dependency: self, - user: user.asRecord, - buttonState: state - ) + static func responseToUserViewButtonAction( + dependency: NeedsDependency & AuthContextProvider, + user: Mastodon.Entity.Account, + buttonState: UserView.ButtonState + ) async throws { + switch buttonState { + case .follow: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(user.id) + + case .request: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(user.id) + case .unfollow: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == user.id }) + case .blocked: + try await DataSourceFacade.responseToUserBlockAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(user.id) + + case .pending: + try await DataSourceFacade.responseToUserFollowAction( + dependency: dependency, + user: user + ) + + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == user.id }) + case .none, .loading: + break //no-op } } } diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index 6deeb0a2a..c83838492 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -13,6 +13,7 @@ import MastodonLocalization import MastodonMeta import MastodonCore import Meta +import MastodonSDK extension UserView { public func configure(user: MastodonUser, delegate: UserViewDelegate?) { @@ -63,4 +64,8 @@ extension UserView { .assign(to: \.authorVerifiedLink, on: viewModel) .store(in: &disposeBag) } + + func configure(with account: Mastodon.Entity.Account) { + //TODO: Implement + } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 526e74ab3..d2f62e2fc 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -9,6 +9,8 @@ import UIKit import CoreDataStack import MastodonUI import Combine +import MastodonCore +import MastodonSDK extension UserTableViewCell { final class ViewModel { @@ -72,5 +74,27 @@ extension UserTableViewCell { self.delegate = delegate } - +} + +extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider { + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) { + Task { + try await DataSourceFacade.responseToUserViewButtonAction( + dependency: self, + user: user.asRecord, + buttonState: state + ) + } + } + + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account) { + Task { + try await DataSourceFacade.responseToUserViewButtonAction( + dependency: self, + user: user, + buttonState: state + ) + } + } + } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 45543dd1f..f50509628 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -148,7 +148,98 @@ extension APIService { let response = try result.get() return response } - + + public func toggleBlock( + user: Mastodon.Entity.Account, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + fatalError("Not implemented yet") + +// let managedObjectContext = backgroundManagedObjectContext +// let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { +// let authentication = authenticationBox.authentication +// +// guard +// let user = user.object(in: managedObjectContext), +// let me = authentication.user(in: managedObjectContext) +// else { +// throw APIError.implicit(.badRequest) +// } +// +// let isBlocking = user.blockingBy.contains(me) +// let isFollowing = user.followingBy.contains(me) +// // toggle block state +// user.update(isBlocking: !isBlocking, by: me) +// // update follow state implicitly +// if !isBlocking { +// // will do block action. set to unfollow +// user.update(isFollowing: false, by: me) +// } +// +// return MastodonBlockContext( +// sourceUserID: me.id, +// targetUserID: user.id, +// targetUsername: user.username, +// isBlocking: isBlocking, +// isFollowing: isFollowing +// ) +// } +// +// let result: Result, Error> +// do { +// if blockContext.isBlocking { +// let response = try await Mastodon.API.Account.unblock( +// session: session, +// domain: authenticationBox.domain, +// accountID: blockContext.targetUserID, +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } else { +// let response = try await Mastodon.API.Account.block( +// session: session, +// domain: authenticationBox.domain, +// accountID: blockContext.targetUserID, +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } +// } catch { +// result = .failure(error) +// } +// +// try await managedObjectContext.performChanges { +// let authentication = authenticationBox.authentication +// +// guard +// let user = user.object(in: managedObjectContext), +// let me = authentication.user(in: managedObjectContext) +// else { return } +// +// +// switch result { +// case .success(let response): +// let relationship = response.value +// Persistence.MastodonUser.update( +// mastodonUser: user, +// context: Persistence.MastodonUser.RelationshipContext( +// entity: relationship, +// me: me, +// networkDate: response.networkDate +// ) +// ) +// case .failure: +// // rollback +// user.update(isBlocking: blockContext.isBlocking, by: me) +// user.update(isFollowing: blockContext.isFollowing, by: me) +// } +// } +// +// let response = try result.get() +// return response + } + + } extension MastodonUser { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 8decfe632..027dae1cf 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -38,11 +38,11 @@ extension APIService { let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } guard let user = user.object(in: managedObjectContext) else { return nil } - + let isFollowing = user.followingBy.contains(me) let isPending = user.followRequestedBy.contains(me) let needsUnfollow = isFollowing || isPending - + if needsUnfollow { // unfollow user.update(isFollowing: false, by: me) @@ -66,11 +66,11 @@ extension APIService { ) return context } - + guard let followContext = _followContext else { throw APIError.implicit(.badRequest) } - + // request follow or unfollow let result: Result, Error> do { @@ -85,13 +85,13 @@ extension APIService { } catch { result = .failure(error) } - + // update friendship state try await managedObjectContext.performChanges { guard let me = authenticationBox.authentication.user(in: managedObjectContext), let user = user.object(in: managedObjectContext) else { return } - + switch result { case .success(let response): Persistence.MastodonUser.update( @@ -108,11 +108,105 @@ extension APIService { user.update(isFollowRequested: followContext.isPending, by: me) } } - + let response = try result.get() return response } + public func toggleFollow( + user: Mastodon.Entity.Account, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + fatalError("Not implemented yet") + + /** + 1. Get relation between me and user + 2. check if I follow them: + if so: unfollow + if not: follow + 3. return result of 2. + + */ + +// let managedObjectContext = backgroundManagedObjectContext +// let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { +// guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } +// guard let user = user.object(in: managedObjectContext) else { return nil } +// +// let isFollowing = user.followingBy.contains(me) +// let isPending = user.followRequestedBy.contains(me) +// let needsUnfollow = isFollowing || isPending +// +// if needsUnfollow { +// // unfollow +// user.update(isFollowing: false, by: me) +// user.update(isFollowRequested: false, by: me) +// } else { +// // follow +// if user.locked { +// user.update(isFollowing: false, by: me) +// user.update(isFollowRequested: true, by: me) +// } else { +// user.update(isFollowing: true, by: me) +// user.update(isFollowRequested: false, by: me) +// } +// } +// let context = MastodonFollowContext( +// sourceUserID: me.id, +// targetUserID: user.id, +// isFollowing: isFollowing, +// isPending: isPending, +// needsUnfollow: needsUnfollow +// ) +// return context +// } +// +// guard let followContext = _followContext else { +// throw APIError.implicit(.badRequest) +// } +// +// // request follow or unfollow +// let result: Result, Error> +// do { +// let response = try await Mastodon.API.Account.follow( +// session: session, +// domain: authenticationBox.domain, +// accountID: followContext.targetUserID, +// followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()), +// authorization: authenticationBox.userAuthorization +// ).singleOutput() +// result = .success(response) +// } catch { +// result = .failure(error) +// } +// +// // update friendship state +// try await managedObjectContext.performChanges { +// guard let me = authenticationBox.authentication.user(in: managedObjectContext), +// let user = user.object(in: managedObjectContext) +// else { return } +// +// switch result { +// case .success(let response): +// Persistence.MastodonUser.update( +// mastodonUser: user, +// context: Persistence.MastodonUser.RelationshipContext( +// entity: response.value, +// me: me, +// networkDate: response.networkDate +// ) +// ) +// case .failure: +// // rollback +// user.update(isFollowing: followContext.isFollowing, by: me) +// user.update(isFollowRequested: followContext.isPending, by: me) +// } +// } +// +// let response = try result.get() +// return response + } + public func toggleShowReblogs( for user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index 4127ed4d4..c4c2edd9a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -13,6 +13,7 @@ import MastodonCore import MastodonMeta import MastodonAsset import MastodonLocalization +import MastodonSDK extension UserView { public final class ViewModel: ObservableObject { @@ -26,6 +27,7 @@ extension UserView { @Published public var authorFollowers: Int? @Published public var authorVerifiedLink: String? @Published public var user: MastodonUser? + @Published public var account: Mastodon.Entity.Account? } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index cd07b3004..186470b4a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -12,9 +12,11 @@ import MastodonAsset import MastodonLocalization import os import CoreDataStack +import MastodonSDK public protocol UserViewDelegate: AnyObject { func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account) } public final class UserView: UIView { @@ -251,9 +253,12 @@ public extension UserView { } } - @objc private func didTapButton() { - guard let user = viewModel.user else { return } - delegate?.userView(self, didTapButtonWith: currentButtonState, for: user) + @objc private func didTapFollowButton() { + if let user = viewModel.user { + delegate?.userView(self, didTapButtonWith: currentButtonState, for: user) + } else if let account = viewModel.account { + delegate?.userView(self, didTapButtonWith: currentButtonState, for: account) + } } func setButtonState(_ state: ButtonState) { @@ -310,7 +315,8 @@ public extension UserView { followButton.configuration?.baseBackgroundColor = .clear } - followButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) + followButton.addTarget(self, action: #selector(didTapFollowButton), for: .touchUpInside) followButton.titleLabel?.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15)) } } + From 1750ef83a689a0c9dae5625d923ceaa58c0fc7d5 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 19 Oct 2023 15:12:24 +0200 Subject: [PATCH 05/24] Implement follow/unfollow and block/unblock for `Mastodon.Entity.Account` --- .../Service/API/APIService+Block.swift | 107 ++++------------- .../Service/API/APIService+Follow.swift | 108 ++++-------------- .../Service/API/APIService+Relationship.swift | 32 +++++- 3 files changed, 72 insertions(+), 175 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index f50509628..c1650e9b5 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -153,93 +153,30 @@ extension APIService { user: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - fatalError("Not implemented yet") + guard let relationship = try await relationship(forAccounts: [user], authenticationBox: authenticationBox).value.first else { + throw APIError.implicit(.badRequest) + } -// let managedObjectContext = backgroundManagedObjectContext -// let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { -// let authentication = authenticationBox.authentication -// -// guard -// let user = user.object(in: managedObjectContext), -// let me = authentication.user(in: managedObjectContext) -// else { -// throw APIError.implicit(.badRequest) -// } -// -// let isBlocking = user.blockingBy.contains(me) -// let isFollowing = user.followingBy.contains(me) -// // toggle block state -// user.update(isBlocking: !isBlocking, by: me) -// // update follow state implicitly -// if !isBlocking { -// // will do block action. set to unfollow -// user.update(isFollowing: false, by: me) -// } -// -// return MastodonBlockContext( -// sourceUserID: me.id, -// targetUserID: user.id, -// targetUsername: user.username, -// isBlocking: isBlocking, -// isFollowing: isFollowing -// ) -// } -// -// let result: Result, Error> -// do { -// if blockContext.isBlocking { -// let response = try await Mastodon.API.Account.unblock( -// session: session, -// domain: authenticationBox.domain, -// accountID: blockContext.targetUserID, -// authorization: authenticationBox.userAuthorization -// ).singleOutput() -// result = .success(response) -// } else { -// let response = try await Mastodon.API.Account.block( -// session: session, -// domain: authenticationBox.domain, -// accountID: blockContext.targetUserID, -// authorization: authenticationBox.userAuthorization -// ).singleOutput() -// result = .success(response) -// } -// } catch { -// result = .failure(error) -// } -// -// try await managedObjectContext.performChanges { -// let authentication = authenticationBox.authentication -// -// guard -// let user = user.object(in: managedObjectContext), -// let me = authentication.user(in: managedObjectContext) -// else { return } -// -// -// switch result { -// case .success(let response): -// let relationship = response.value -// Persistence.MastodonUser.update( -// mastodonUser: user, -// context: Persistence.MastodonUser.RelationshipContext( -// entity: relationship, -// me: me, -// networkDate: response.networkDate -// ) -// ) -// case .failure: -// // rollback -// user.update(isBlocking: blockContext.isBlocking, by: me) -// user.update(isFollowing: blockContext.isFollowing, by: me) -// } -// } -// -// let response = try result.get() -// return response + let response: Mastodon.Response.Content + + if relationship.blocking { + response = try await Mastodon.API.Account.unblock( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + authorization: authenticationBox.userAuthorization + ).singleOutput() + } else { + response = try await Mastodon.API.Account.block( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + authorization: authenticationBox.userAuthorization + ).singleOutput() + } + + return response } - - } extension MastodonUser { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 027dae1cf..e31dbedce 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -117,94 +117,32 @@ extension APIService { user: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - fatalError("Not implemented yet") - /** - 1. Get relation between me and user - 2. check if I follow them: - if so: unfollow - if not: follow - 3. return result of 2. + guard let relationship = try await relationship(forAccounts: [user], authenticationBox: authenticationBox).value.first else { + throw APIError.implicit(.badRequest) + } - */ + let response: Mastodon.Response.Content -// let managedObjectContext = backgroundManagedObjectContext -// let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { -// guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } -// guard let user = user.object(in: managedObjectContext) else { return nil } -// -// let isFollowing = user.followingBy.contains(me) -// let isPending = user.followRequestedBy.contains(me) -// let needsUnfollow = isFollowing || isPending -// -// if needsUnfollow { -// // unfollow -// user.update(isFollowing: false, by: me) -// user.update(isFollowRequested: false, by: me) -// } else { -// // follow -// if user.locked { -// user.update(isFollowing: false, by: me) -// user.update(isFollowRequested: true, by: me) -// } else { -// user.update(isFollowing: true, by: me) -// user.update(isFollowRequested: false, by: me) -// } -// } -// let context = MastodonFollowContext( -// sourceUserID: me.id, -// targetUserID: user.id, -// isFollowing: isFollowing, -// isPending: isPending, -// needsUnfollow: needsUnfollow -// ) -// return context -// } -// -// guard let followContext = _followContext else { -// throw APIError.implicit(.badRequest) -// } -// -// // request follow or unfollow -// let result: Result, Error> -// do { -// let response = try await Mastodon.API.Account.follow( -// session: session, -// domain: authenticationBox.domain, -// accountID: followContext.targetUserID, -// followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()), -// authorization: authenticationBox.userAuthorization -// ).singleOutput() -// result = .success(response) -// } catch { -// result = .failure(error) -// } -// -// // update friendship state -// try await managedObjectContext.performChanges { -// guard let me = authenticationBox.authentication.user(in: managedObjectContext), -// let user = user.object(in: managedObjectContext) -// else { return } -// -// switch result { -// case .success(let response): -// Persistence.MastodonUser.update( -// mastodonUser: user, -// context: Persistence.MastodonUser.RelationshipContext( -// entity: response.value, -// me: me, -// networkDate: response.networkDate -// ) -// ) -// case .failure: -// // rollback -// user.update(isFollowing: followContext.isFollowing, by: me) -// user.update(isFollowRequested: followContext.isPending, by: me) -// } -// } -// -// let response = try result.get() -// return response + if relationship.following || (relationship.requested ?? false) { + // unfollow + response = try await Mastodon.API.Account.unfollow( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + authorization: authenticationBox.userAuthorization + ).singleOutput() + } else { + response = try await Mastodon.API.Account.follow( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + followQueryType: .follow(query: .init()), + authorization: authenticationBox.userAuthorization + ).singleOutput() + } + + return response } public func toggleShowReblogs( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index 370ed4fcf..2df898977 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -18,7 +18,7 @@ extension APIService { authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { let managedObjectContext = backgroundManagedObjectContext - + let _query: Mastodon.API.Account.RelationshipQuery? = try? await managedObjectContext.perform { var ids: [MastodonUser.ID] = [] for record in records { @@ -32,14 +32,14 @@ extension APIService { guard let query = _query else { throw APIError.implicit(.badRequest) } - + let response = try await Mastodon.API.Account.relationships( session: session, domain: authenticationBox.domain, query: query, authorization: authenticationBox.userAuthorization ).singleOutput() - + try await managedObjectContext.performChanges { guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { // assertionFailure() @@ -50,7 +50,7 @@ extension APIService { for record in records { guard let user = record.object(in: managedObjectContext) else { continue } guard let relationship = relationships.first(where: { $0.id == user.id }) else { continue } - + Persistence.MastodonUser.update( mastodonUser: user, context: Persistence.MastodonUser.RelationshipContext( @@ -64,5 +64,27 @@ extension APIService { return response } - + + + public func relationship( + forAccounts accounts: [Mastodon.Entity.Account], + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { + + let ids: [MastodonUser.ID] = accounts.compactMap { $0.id } + + guard ids.isEmpty == false else { throw APIError.implicit(.badRequest) } + + let query = Mastodon.API.Account.RelationshipQuery(ids: ids) + + let response = try await Mastodon.API.Account.relationships( + session: session, + domain: authenticationBox.domain, + query: query, + authorization: authenticationBox.userAuthorization + ).singleOutput() + + return response + } + } From a549534fcfa573fea98438507142e5d813fe78b1 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 19 Oct 2023 16:16:18 +0200 Subject: [PATCH 06/24] Get (and show) account-entities on followings-list This is a first step, for now we show the name to see if it works (and it does!), the other properties and functionality will follow. Again, this includes some refactoring, like getting rid of Configuration --- Mastodon/Diffable/User/UserItem.swift | 2 + Mastodon/Diffable/User/UserSection.swift | 31 +++---- .../FamiliarFollowersViewModel+Diffable.swift | 4 +- .../FollowerListViewModel+Diffable.swift | 4 +- .../FollowingListViewModel+Diffable.swift | 38 ++++---- .../FollowingListViewModel+State.swift | 90 +++++++++---------- .../Following/FollowingListViewModel.swift | 15 ++-- .../UserLIst/UserListViewModel+Diffable.swift | 4 +- .../View/Content/UserView+Configuration.swift | 7 ++ .../UserTableViewCell+ViewModel.swift | 12 +++ 10 files changed, 108 insertions(+), 99 deletions(-) diff --git a/Mastodon/Diffable/User/UserItem.swift b/Mastodon/Diffable/User/UserItem.swift index ff533d897..2bc5f33a6 100644 --- a/Mastodon/Diffable/User/UserItem.swift +++ b/Mastodon/Diffable/User/UserItem.swift @@ -8,9 +8,11 @@ import Foundation import CoreData import CoreDataStack +import MastodonSDK enum UserItem: Hashable { case user(record: ManagedObjectRecord) + case account(account: Mastodon.Entity.Account) case bottomLoader case bottomHeader(text: String) } diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index e6632c337..5667b3450 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -19,15 +19,11 @@ enum UserSection: Hashable { } extension UserSection { - struct Configuration { - weak var userTableViewCellDelegate: UserTableViewCellDelegate? - } - static func diffableDataSource( tableView: UITableView, context: AppContext, authContext: AuthContext, - configuration: Configuration + userTableViewCellDelegate: UserTableViewCellDelegate? ) -> UITableViewDiffableDataSource { tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) @@ -35,6 +31,12 @@ extension UserSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { + case .account(let account): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + cell.configure(tableView: tableView, account: account, delegate: userTableViewCellDelegate) + + return cell + case .user(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell context.managedObjectContext.performAndWait { @@ -50,7 +52,7 @@ extension UserSection { blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() ), - configuration: configuration + userTableViewCellDelegate: userTableViewCellDelegate ) } @@ -60,13 +62,12 @@ extension UserSection { cell.startAnimating() return cell case .bottomHeader(let text): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell - cell.messageLabel.text = text - return cell - } // end switch - } // end UITableViewDiffableDataSource - } // end static func tableViewDiffableDataSource { … } - + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell + cell.messageLabel.text = text + return cell + } + } + } } extension UserSection { @@ -77,13 +78,13 @@ extension UserSection { tableView: UITableView, cell: UserTableViewCell, viewModel: UserTableViewCell.ViewModel, - configuration: Configuration + userTableViewCellDelegate: UserTableViewCellDelegate? ) { cell.configure( me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), tableView: tableView, viewModel: viewModel, - delegate: configuration.userTableViewCellDelegate + delegate: userTableViewCellDelegate ) } diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel+Diffable.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel+Diffable.swift index 0b0428c67..291a6b3ff 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel+Diffable.swift @@ -17,9 +17,7 @@ extension FamiliarFollowersViewModel { tableView: tableView, context: context, authContext: authContext, - configuration: UserSection.Configuration( - userTableViewCellDelegate: userTableViewCellDelegate - ) + userTableViewCellDelegate: userTableViewCellDelegate ) userFetchedResultsController.$records diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift index 11276c04f..d0676dc59 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift @@ -18,9 +18,7 @@ extension FollowerListViewModel { tableView: tableView, context: context, authContext: authContext, - configuration: UserSection.Configuration( - userTableViewCellDelegate: userTableViewCellDelegate - ) + userTableViewCellDelegate: userTableViewCellDelegate ) // workaround to append loader wrong animation issue diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift index 785937335..d40c37ae8 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift @@ -19,9 +19,7 @@ extension FollowingListViewModel { tableView: tableView, context: context, authContext: authContext, - configuration: UserSection.Configuration( - userTableViewCellDelegate: userTableViewCellDelegate - ) + userTableViewCellDelegate: userTableViewCellDelegate ) // workaround to append loader wrong animation issue @@ -31,30 +29,32 @@ extension FollowingListViewModel { snapshot.appendItems([.bottomLoader], toSection: .main) diffableDataSource?.applySnapshotUsingReloadData(snapshot) - userFetchedResultsController.$records + $accounts .receive(on: DispatchQueue.main) - .sink { [weak self] records in - guard let self = self else { return } + .sink { [weak self] accounts in + guard let self else { return } guard let diffableDataSource = self.diffableDataSource else { return } - + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - let items = records.map { UserItem.user(record: $0) } + let items = accounts.map { UserItem.account(account: $0) } snapshot.appendItems(items, toSection: .main) if let currentState = self.stateMachine.currentState { switch currentState { - case is State.Idle, is State.Loading, is State.Fail: - snapshot.appendItems([.bottomLoader], toSection: .main) - case is State.NoMore: - guard let userID = self.userID, - userID != self.authContext.mastodonAuthenticationBox.userID - else { break } - // display footer exclude self - let text = L10n.Scene.Following.footer - snapshot.appendItems([.bottomHeader(text: text)], toSection: .main) - default: - break + case is State.Loading: + snapshot.appendItems([.bottomLoader], toSection: .main) + case is State.NoMore: + guard let userID = self.userID, + userID != self.authContext.mastodonAuthenticationBox.userID + else { break } + // display footer exclude self + let text = L10n.Scene.Following.footer + snapshot.appendItems([.bottomHeader(text: text)], toSection: .main) + case is State.Idle, is State.Fail: + break + default: + break } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift index df71c0d7c..32856c643 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift @@ -11,7 +11,7 @@ import MastodonSDK extension FollowingListViewModel { class State: GKState { - + let id = UUID() weak var viewModel: FollowingListViewModel? @@ -32,10 +32,10 @@ extension FollowingListViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { guard let viewModel = viewModel else { return false } switch stateClass { - case is Reloading.Type: - return viewModel.userID != nil - default: - return false + case is Reloading.Type: + return viewModel.userID != nil + default: + return false } } } @@ -43,19 +43,19 @@ extension FollowingListViewModel.State { class Reloading: FollowingListViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { - case is Loading.Type: - return true - default: - return false + case is Loading.Type: + return true + default: + return false } } override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel, let stateMachine else { return } // reset - viewModel.userFetchedResultsController.userIDs = [] + viewModel.accounts = [] stateMachine.enter(Loading.self) } @@ -65,10 +65,10 @@ extension FollowingListViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { - case is Loading.Type: - return true - default: - return false + case is Loading.Type: + return true + default: + return false } } @@ -85,10 +85,10 @@ extension FollowingListViewModel.State { class Idle: FollowingListViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { - case is Reloading.Type, is Loading.Type: - return true - default: - return false + case is Reloading.Type, is Loading.Type: + return true + default: + return false } } } @@ -99,14 +99,14 @@ extension FollowingListViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { - case is Fail.Type: - return true - case is Idle.Type: - return true - case is NoMore.Type: - return true - default: - return false + case is Fail.Type: + return true + case is Idle.Type: + return true + case is NoMore.Type: + return true + default: + return false } } @@ -117,9 +117,9 @@ extension FollowingListViewModel.State { maxID = nil } - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel, let stateMachine else { return } - guard let userID = viewModel.userID, !userID.isEmpty else { + guard let userID = viewModel.userID, userID.isEmpty == false else { stateMachine.enter(Fail.self) return } @@ -131,15 +131,17 @@ extension FollowingListViewModel.State { maxID: maxID, authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) - + var hasNewAppend = false - var userIDs = viewModel.userFetchedResultsController.userIDs + var accounts = viewModel.accounts + for user in response.value { - guard !userIDs.contains(user.id) else { continue } - userIDs.append(user.id) + guard accounts.contains(user) == false else { continue } + accounts.append(user) hasNewAppend = true } - + + let maxID = response.link?.maxID if hasNewAppend, maxID != nil { @@ -147,28 +149,24 @@ extension FollowingListViewModel.State { } else { await enter(state: NoMore.self) } - self.maxID = maxID - viewModel.userFetchedResultsController.userIDs = userIDs + viewModel.accounts = accounts + self.maxID = maxID } catch { await enter(state: Fail.self) } - } // end Task - } // end func didEnter + } + } } class NoMore: FollowingListViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { - case is Reloading.Type: - return true - default: - return false + case is Reloading.Type: + return true + default: + return false } } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - } } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift index e8758e645..9b634c9a6 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift @@ -20,9 +20,9 @@ final class FollowingListViewModel { // input let context: AppContext let authContext: AuthContext - let userFetchedResultsController: UserFetchedResultsController - let listBatchFetchViewModel = ListBatchFetchViewModel() - + @Published var accounts: [Mastodon.Entity.Account] + let listBatchFetchViewModel: ListBatchFetchViewModel + @Published var domain: String? @Published var userID: String? @@ -49,14 +49,9 @@ final class FollowingListViewModel { ) { self.context = context self.authContext = authContext - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: domain, - additionalPredicate: nil - ) self.domain = domain self.userID = userID - // super.init() - + self.accounts = [] + self.listBatchFetchViewModel = ListBatchFetchViewModel() } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift index bbc46063a..d4830affc 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift @@ -20,9 +20,7 @@ extension UserListViewModel { tableView: tableView, context: context, authContext: authContext, - configuration: UserSection.Configuration( - userTableViewCellDelegate: userTableViewCellDelegate - ) + userTableViewCellDelegate: userTableViewCellDelegate ) // workaround to append loader wrong animation issue diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index c83838492..90633bae0 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -19,6 +19,7 @@ extension UserView { public func configure(user: MastodonUser, delegate: UserViewDelegate?) { self.delegate = delegate viewModel.user = user + viewModel.account = nil Publishers.CombineLatest( user.publisher(for: \.avatar), @@ -67,5 +68,11 @@ extension UserView { func configure(with account: Mastodon.Entity.Account) { //TODO: Implement + viewModel.account = account + viewModel.user = nil + + // username + let metaContent = PlaintextMetaContent(string: "@\(account.username)") + authorUsernameLabel.configure(content: metaContent) } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index d2f62e2fc..5d3e9c2c8 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -31,6 +31,18 @@ extension UserTableViewCell { extension UserTableViewCell { + func configure( + me: MastodonUser? = nil, + tableView: UITableView, + account: Mastodon.Entity.Account, + delegate: UserTableViewCellDelegate? + ) { + //TODO: Implement + userView.configure(with: account) + } + + + //TODO: Duplicate func configure( me: MastodonUser? = nil, tableView: UITableView, From 5058bac018a41ad846fe5ae73a2dd74b015021f9 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 20 Oct 2023 12:30:49 +0200 Subject: [PATCH 07/24] Migrate Kanna over to SPM --- Mastodon.xcodeproj/project.pbxproj | 53 ++++++------------- .../xcshareddata/swiftpm/Package.resolved | 9 ++++ Podfile | 1 - Podfile.lock | 6 +-- 4 files changed, 27 insertions(+), 42 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 307d3a540..c9e7080f7 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -147,6 +147,7 @@ D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; }; D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */; }; D852C23E2AC5D03300309232 /* InstanceRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23D2AC5D03300309232 /* InstanceRulesViewController.swift */; }; + D87364F92AE28DB500C8F919 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = D87364F82AE28DB500C8F919 /* Kanna */; }; D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; }; D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; }; D87BFC8F291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */; }; @@ -1289,6 +1290,7 @@ files = ( 357FEEAF29523D470021C9DC /* MastodonSDKDynamic in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, + D87364F92AE28DB500C8F919 /* Kanna in Frameworks */, 71458AF57697DB405CFEC37C /* Pods_Mastodon.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3120,7 +3122,6 @@ buildConfigurationList = DB427DFC25BAA00100D1B89D /* Build configuration list for PBXNativeTarget "Mastodon" */; buildPhases = ( 7A04933A2AB1D5B758D4F908 /* [CP] Check Pods Manifest.lock */, - 5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */, DB427DD025BAA00100D1B89D /* Resources */, DB427DCE25BAA00100D1B89D /* Sources */, DB427DCF25BAA00100D1B89D /* Frameworks */, @@ -3142,6 +3143,7 @@ name = Mastodon; packageProductDependencies = ( 357FEEAE29523D470021C9DC /* MastodonSDKDynamic */, + D87364F82AE28DB500C8F919 /* Kanna */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -3178,7 +3180,6 @@ DB427DEF25BAA00100D1B89D /* Sources */, DB427DF025BAA00100D1B89D /* Frameworks */, DB427DF125BAA00100D1B89D /* Resources */, - ECC2E90D421B45415C311BED /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -3326,6 +3327,7 @@ mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( 2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */, + D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -3419,23 +3421,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Mastodon/Pods-Mastodon-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Mastodon/Pods-Mastodon-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Mastodon/Pods-Mastodon-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 6E033728B42BA1C0018B6131 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3556,23 +3541,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - ECC2E90D421B45415C311BED /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -5463,6 +5431,14 @@ kind = branch; }; }; + D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/tid-kijyun/Kanna.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.2.7; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -5499,6 +5475,11 @@ isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; }; + D87364F82AE28DB500C8F919 /* Kanna */ = { + isa = XCSwiftPackageProductDependency; + package = D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */; + productName = Kanna; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = DB427DCA25BAA00100D1B89D /* Project object */; diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6e737d55c..de3b63256 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -55,6 +55,15 @@ "version": "1.6.0" } }, + { + "package": "Kanna", + "repositoryURL": "https://github.com/tid-kijyun/Kanna.git", + "state": { + "branch": null, + "revision": "f9e4922223dd0d3dfbf02ca70812cf5531fc0593", + "version": "5.2.7" + } + }, { "package": "KeychainAccess", "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", diff --git a/Podfile b/Podfile index e9538aa3d..7ecc76a7d 100644 --- a/Podfile +++ b/Podfile @@ -9,7 +9,6 @@ target 'Mastodon' do # misc pod 'SwiftGen', '~> 6.6.2' - pod 'Kanna', '~> 5.2.2' pod 'Sourcery', '~> 1.9' target 'MastodonTests' do diff --git a/Podfile.lock b/Podfile.lock index 24522c82e..59c3e48c9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,26 +1,22 @@ PODS: - - Kanna (5.2.7) - Sourcery (1.9.2): - Sourcery/CLI-Only (= 1.9.2) - Sourcery/CLI-Only (1.9.2) - SwiftGen (6.6.2) DEPENDENCIES: - - Kanna (~> 5.2.2) - Sourcery (~> 1.9) - SwiftGen (~> 6.6.2) SPEC REPOS: trunk: - - Kanna - Sourcery - SwiftGen SPEC CHECKSUMS: - Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 Sourcery: 179539341c2261068528cd15a31837b7238fd901 SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c -PODFILE CHECKSUM: 597c21d7aa08efec996048577c3c4fbeffbb6305 +PODFILE CHECKSUM: ee2c03fbf7eb6e4ee75d97b1309e9ed6dfcf5cdd COCOAPODS: 1.13.0 From 7432b6f22b2aa28f80735772b8d210bfc4ce32ce Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 20 Oct 2023 17:49:48 +0200 Subject: [PATCH 08/24] Show account-information from entities --- .../View/Content/UserView+Configuration.swift | 48 +++++++++++++++++-- .../MastodonUI/View/Content/UserView.swift | 5 +- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index 90633bae0..d67f2ec67 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -14,6 +14,7 @@ import MastodonMeta import MastodonCore import Meta import MastodonSDK +import MastodonAsset extension UserView { public func configure(user: MastodonUser, delegate: UserViewDelegate?) { @@ -67,12 +68,51 @@ extension UserView { } func configure(with account: Mastodon.Entity.Account) { - //TODO: Implement viewModel.account = account viewModel.user = nil - // username - let metaContent = PlaintextMetaContent(string: "@\(account.username)") - authorUsernameLabel.configure(content: metaContent) + let authorUsername = PlaintextMetaContent(string: "@\(account.username)") + authorUsernameLabel.configure(content: authorUsername) + + do { + let emojis = account.emojis?.asDictionary ?? [:] + let content = MastodonContent(content: account.displayNameWithFallback, emojis: emojis) + let metaContent = try MastodonMetaContent.convert(document: content) + authorNameLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: account.displayNameWithFallback) + authorNameLabel.configure(content: metaContent) + } + + if let imageURL = account.avatarImageURL() { + avatarButton.avatarImageView.af.setImage(withURL: imageURL) + } + + let count = account.followersCount + authorFollowersLabel.attributedText = NSAttributedString( + format: NSAttributedString(string: L10n.Common.UserList.followersCount("%@"), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))]), + args: NSAttributedString(string: UserView.metricFormatter.string(from: count) ?? count.formatted(), attributes: [.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold))]) + ) + + if let verifiedLinkField = account.verifiedLink { + let link = verifiedLinkField.value + + authorVerifiedImageView.image = UIImage(systemName: "checkmark") + authorVerifiedImageView.tintColor = Asset.Colors.Brand.blurple.color + authorVerifiedLabel.textColor = Asset.Colors.Brand.blurple.color + do { + let mastodonContent = MastodonContent(content: link, emojis: [:]) + let content = try MastodonMetaContent.convert(document: mastodonContent) + authorVerifiedLabel.configure(content: content) + } catch { + let content = PlaintextMetaContent(string: link) + authorVerifiedLabel.configure(content: content) + } + } else { + authorVerifiedImageView.image = UIImage(systemName: "questionmark.circle") + authorVerifiedImageView.tintColor = .secondaryLabel + authorVerifiedLabel.configure(content: PlaintextMetaContent(string: L10n.Common.UserList.noVerifiedLink)) + authorVerifiedLabel.textColor = .secondaryLabel + } } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index 186470b4a..9a0a4d389 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -26,7 +26,8 @@ public final class UserView: UIView { } private var currentButtonState: ButtonState = .none - + public static var metricFormatter = MastodonMetricFormatter() + public weak var delegate: UserViewDelegate? public var disposeBag = Set() @@ -102,7 +103,7 @@ public final class UserView: UIView { return label }() - private let followButtonWrapper = UIView() + public let followButtonWrapper = UIView() private let followButton: UIButton = { var buttonConfiguration = UIButton.Configuration.filled() buttonConfiguration.background.cornerRadius = 10 From c26467c90459be7b9d3d5daf6f13efae273346f0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 20 Oct 2023 18:53:03 +0200 Subject: [PATCH 09/24] Add spinner to loading-state (and fix some colors) --- .../MastodonUI/View/Content/UserView.swift | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index 9a0a4d389..242f6c44d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -104,7 +104,7 @@ public final class UserView: UIView { }() public let followButtonWrapper = UIView() - private let followButton: UIButton = { + public let followButton: UIButton = { var buttonConfiguration = UIButton.Configuration.filled() buttonConfiguration.background.cornerRadius = 10 @@ -272,48 +272,63 @@ public extension UserView { followButtonWrapper.isHidden = false followButton.isHidden = false followButton.configuration?.title = nil - followButton.setBackgroundColor(Asset.Colors.Button.disabled.color, for: .normal) + followButton.configuration?.showsActivityIndicator = true + followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollowing.color + followButton.configuration?.baseForegroundColor = Asset.Colors.Brand.blurple.color + followButton.isEnabled = false case .follow: followButtonWrapper.isHidden = false followButton.isHidden = false followButton.configuration?.title = L10n.Common.Controls.Friendship.follow - followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userFollow.color + followButton.configuration?.showsActivityIndicator = false + followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollow.color followButton.configuration?.baseForegroundColor = .white + followButton.isEnabled = true case .request: followButtonWrapper.isHidden = false followButton.isHidden = false followButton.configuration?.title = L10n.Common.Controls.Friendship.request - followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userFollow.color + followButton.configuration?.showsActivityIndicator = false + followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollow.color followButton.configuration?.baseForegroundColor = .white + followButton.isEnabled = true case .pending: followButtonWrapper.isHidden = false followButton.isHidden = false followButton.configuration?.title = L10n.Common.Controls.Friendship.pending followButton.configuration?.baseForegroundColor = Asset.Colors.Button.userFollowingTitle.color - followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userFollowing.color + followButton.configuration?.showsActivityIndicator = false + followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollowing.color + followButton.isEnabled = true case .unfollow: followButtonWrapper.isHidden = false followButton.isHidden = false followButton.configuration?.title = L10n.Common.Controls.Friendship.following - followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userFollowing.color + followButton.configuration?.showsActivityIndicator = false + followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userFollowing.color followButton.configuration?.baseForegroundColor = Asset.Colors.Button.userFollowingTitle.color + followButton.isEnabled = true case .blocked: followButtonWrapper.isHidden = false followButton.isHidden = false followButton.configuration?.title = L10n.Common.Controls.Friendship.blocked - followButton.configuration?.baseBackgroundColor = Asset.Colors.Button.userBlocked.color + followButton.configuration?.showsActivityIndicator = false + followButton.configuration?.background.backgroundColor = Asset.Colors.Button.userBlocked.color followButton.configuration?.baseForegroundColor = .systemRed + followButton.isEnabled = true case .none: followButtonWrapper.isHidden = true followButton.isHidden = true followButton.configuration?.title = nil - followButton.configuration?.baseBackgroundColor = .clear + followButton.configuration?.showsActivityIndicator = false + followButton.configuration?.background.backgroundColor = .clear + followButton.isEnabled = false } followButton.addTarget(self, action: #selector(didTapFollowButton), for: .touchUpInside) From 480d1c5265adbad2d28eafa85e8bd995f944c130 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 20 Oct 2023 18:53:11 +0200 Subject: [PATCH 10/24] Deselect table --- .../Provider/DataSourceProvider+UITableViewDelegate.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 611223df8..c45ef4ca1 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -14,6 +14,7 @@ import MastodonLocalization extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider { func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true ) Task { let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath) guard let item = await item(from: source) else { From fa34df26df0949e48f98a11c455f30909df3d7d7 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 20 Oct 2023 18:53:27 +0200 Subject: [PATCH 11/24] Determine button-state based on freshly loaded relationship --- Mastodon/Diffable/User/UserSection.swift | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index 5667b3450..3c6c962af 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -34,6 +34,36 @@ extension UserSection { case .account(let account): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell cell.configure(tableView: tableView, account: account, delegate: userTableViewCellDelegate) + cell.userView.setButtonState(.loading) + Task { + do { + + guard let relationship = try await context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first else { + return await MainActor.run { + cell.userView.setButtonState(.none) + } + } + + let buttonState: UserView.ButtonState + if relationship.following { + buttonState = .unfollow + } else if relationship.blocking || (relationship.domainBlocking ?? false) { + buttonState = .blocked + } else if relationship.requested ?? false { + buttonState = .pending + } else { + buttonState = .follow + } + + await MainActor.run { + cell.userView.setButtonState(buttonState) + } + } catch { + await MainActor.run { + cell.userView.setButtonState(.none) + } + } + } return cell From 19d67d6dab4fbf5e50ca831d25d5275346d80f0f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 23 Oct 2023 12:18:03 +0200 Subject: [PATCH 12/24] Don't Ddos servers for fetching each relatinoship individually --- Mastodon/Diffable/User/UserItem.swift | 2 +- Mastodon/Diffable/User/UserSection.swift | 34 ++----------------- .../FollowingListViewModel+Diffable.swift | 10 +++++- .../FollowingListViewModel+State.swift | 23 +++++++++---- .../Following/FollowingListViewModel.swift | 3 ++ .../UserTableViewCell+ViewModel.swift | 18 ++++++++++ .../Entity/Mastodon+Entity+Relationship.swift | 2 +- 7 files changed, 52 insertions(+), 40 deletions(-) diff --git a/Mastodon/Diffable/User/UserItem.swift b/Mastodon/Diffable/User/UserItem.swift index 2bc5f33a6..ba44aa52a 100644 --- a/Mastodon/Diffable/User/UserItem.swift +++ b/Mastodon/Diffable/User/UserItem.swift @@ -12,7 +12,7 @@ import MastodonSDK enum UserItem: Hashable { case user(record: ManagedObjectRecord) - case account(account: Mastodon.Entity.Account) + case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) case bottomLoader case bottomHeader(text: String) } diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index 3c6c962af..d636a88b7 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -31,39 +31,11 @@ extension UserSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { - case .account(let account): + case .account(let account, let relationship): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell - cell.configure(tableView: tableView, account: account, delegate: userTableViewCellDelegate) + cell.userView.setButtonState(.loading) - Task { - do { - - guard let relationship = try await context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first else { - return await MainActor.run { - cell.userView.setButtonState(.none) - } - } - - let buttonState: UserView.ButtonState - if relationship.following { - buttonState = .unfollow - } else if relationship.blocking || (relationship.domainBlocking ?? false) { - buttonState = .blocked - } else if relationship.requested ?? false { - buttonState = .pending - } else { - buttonState = .follow - } - - await MainActor.run { - cell.userView.setButtonState(buttonState) - } - } catch { - await MainActor.run { - cell.userView.setButtonState(.none) - } - } - } + cell.configure(tableView: tableView, account: account, relationship: relationship, delegate: userTableViewCellDelegate) return cell diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift index d40c37ae8..533360520 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift @@ -9,6 +9,7 @@ import UIKit import MastodonAsset import MastodonCore import MastodonLocalization +import MastodonSDK extension FollowingListViewModel { func setupDiffableDataSource( @@ -37,7 +38,14 @@ extension FollowingListViewModel { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - let items = accounts.map { UserItem.account(account: $0) } + + let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in + guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)} + + return (account: account, relationship: relationship) + } + + let items = accountsWithRelationship.map { UserItem.account(account: $0.account, relationship: $0.relationship) } snapshot.appendItems(items, toSection: .main) if let currentState = self.stateMachine.currentState { diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift index 32856c643..924e9db55 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift @@ -126,24 +126,34 @@ extension FollowingListViewModel.State { Task { do { - let response = try await viewModel.context.apiService.following( + let accountResponse = try await viewModel.context.apiService.following( userID: userID, maxID: maxID, authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) - + var hasNewAppend = false + + + let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox) + var accounts = viewModel.accounts - - for user in response.value { + + for user in accountResponse.value { guard accounts.contains(user) == false else { continue } accounts.append(user) hasNewAppend = true } + var relationships = viewModel.relationships + + for relationship in newRelationships.value { + guard relationships.contains(relationship) == false else { continue } + relationships.append(relationship) + } + + let maxID = accountResponse.link?.maxID - let maxID = response.link?.maxID - if hasNewAppend, maxID != nil { await enter(state: Idle.self) } else { @@ -151,6 +161,7 @@ extension FollowingListViewModel.State { } viewModel.accounts = accounts + viewModel.relationships = relationships self.maxID = maxID } catch { await enter(state: Fail.self) diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift index 9b634c9a6..e07d1b08e 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift @@ -21,6 +21,8 @@ final class FollowingListViewModel { let context: AppContext let authContext: AuthContext @Published var accounts: [Mastodon.Entity.Account] + @Published var relationships: [Mastodon.Entity.Relationship] + let listBatchFetchViewModel: ListBatchFetchViewModel @Published var domain: String? @@ -52,6 +54,7 @@ final class FollowingListViewModel { self.domain = domain self.userID = userID self.accounts = [] + self.relationships = [] self.listBatchFetchViewModel = ListBatchFetchViewModel() } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 5d3e9c2c8..0fe470914 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -35,10 +35,28 @@ extension UserTableViewCell { me: MastodonUser? = nil, tableView: UITableView, account: Mastodon.Entity.Account, + relationship: Mastodon.Entity.Relationship?, delegate: UserTableViewCellDelegate? ) { //TODO: Implement userView.configure(with: account) + + let buttonState: UserView.ButtonState + if let relationship { + if relationship.following { + buttonState = .unfollow + } else if relationship.blocking || (relationship.domainBlocking ?? false) { + buttonState = .blocked + } else if relationship.requested ?? false { + buttonState = .pending + } else { + buttonState = .follow + } + } else { + buttonState = .none + } + + userView.setButtonState(buttonState) } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift index 180b5bd6e..f5d2200e7 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift @@ -16,7 +16,7 @@ extension Mastodon.Entity { /// 2021/1/29 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/relationship/) - public struct Relationship: Codable, Sendable { + public struct Relationship: Codable, Sendable, Equatable, Hashable { public typealias ID = String public let id: ID From 0951e658a2dbb2813a0686c3dce79df60f2a98e4 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 23 Oct 2023 13:55:54 +0200 Subject: [PATCH 13/24] Add loading-indicator cause I'm lazy --- Mastodon.xcodeproj/project.pbxproj | 17 ++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++++++ Mastodon/Coordinator/SceneCoordinator.swift | 26 ++++++++++++++++--- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c9e7080f7..7abc2dc80 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */; }; D8318A8A2A4468DC00C0FB73 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A892A4468DC00C0FB73 /* AboutViewController.swift */; }; D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; }; + D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = D84FA0922AE6915800987F47 /* MBProgressHUD */; }; D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */; }; D852C23E2AC5D03300309232 /* InstanceRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23D2AC5D03300309232 /* InstanceRulesViewController.swift */; }; D87364F92AE28DB500C8F919 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = D87364F82AE28DB500C8F919 /* Kanna */; }; @@ -1290,6 +1291,7 @@ files = ( 357FEEAF29523D470021C9DC /* MastodonSDKDynamic in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, + D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */, D87364F92AE28DB500C8F919 /* Kanna in Frameworks */, 71458AF57697DB405CFEC37C /* Pods_Mastodon.framework in Frameworks */, ); @@ -3144,6 +3146,7 @@ packageProductDependencies = ( 357FEEAE29523D470021C9DC /* MastodonSDKDynamic */, D87364F82AE28DB500C8F919 /* Kanna */, + D84FA0922AE6915800987F47 /* MBProgressHUD */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -3328,6 +3331,7 @@ packageReferences = ( 2AB501192992322500346092 /* XCRemoteSwiftPackageReference "LightChart" */, D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */, + D84FA0912AE6915800987F47 /* XCRemoteSwiftPackageReference "MBProgressHUD" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -5431,6 +5435,14 @@ kind = branch; }; }; + D84FA0912AE6915800987F47 /* XCRemoteSwiftPackageReference "MBProgressHUD" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jdg/MBProgressHUD.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.0; + }; + }; D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/tid-kijyun/Kanna.git"; @@ -5475,6 +5487,11 @@ isa = XCSwiftPackageProductDependency; productName = MastodonSDKDynamic; }; + D84FA0922AE6915800987F47 /* MBProgressHUD */ = { + isa = XCSwiftPackageProductDependency; + package = D84FA0912AE6915800987F47 /* XCRemoteSwiftPackageReference "MBProgressHUD" */; + productName = MBProgressHUD; + }; D87364F82AE28DB500C8F919 /* Kanna */ = { isa = XCSwiftPackageProductDependency; package = D87364F72AE28DB500C8F919 /* XCRemoteSwiftPackageReference "Kanna" */; diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index de3b63256..ec05f44da 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -82,6 +82,15 @@ "version": null } }, + { + "package": "MBProgressHUD", + "repositoryURL": "https://github.com/jdg/MBProgressHUD.git", + "state": { + "branch": null, + "revision": "bca42b801100b2b3a4eda0ba8dd33d858c780b0d", + "version": "1.2.0" + } + }, { "package": "MetaTextKit", "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 223ac4588..f821aea1f 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -13,6 +13,7 @@ import MastodonSDK import MastodonCore import MastodonAsset import MastodonLocalization +import MBProgressHUD final public class SceneCoordinator { @@ -28,7 +29,8 @@ final public class SceneCoordinator { private(set) weak var tabBarController: MainTabBarController! private(set) weak var splitViewController: RootSplitViewController? - + private(set) weak var rootViewController: UIViewController? + private(set) var secondaryStackHashValues = Set() var childCoordinator: Coordinator? @@ -198,7 +200,7 @@ extension SceneCoordinator { case safari(url: URL) case alertController(alertController: UIAlertController) case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?) - + var isOnboarding: Bool { switch self { case .welcome, @@ -239,6 +241,7 @@ extension SceneCoordinator { rootViewController = splitViewController } sceneDelegate.window?.rootViewController = rootViewController // base: main + self.rootViewController = rootViewController if _authContext == nil { // entry #1: welcome DispatchQueue.main.async { @@ -559,13 +562,30 @@ private extension SceneCoordinator { return viewController } - + private func setupDependency(for needs: NeedsDependency?) { needs?.context = appContext needs?.coordinator = self } } +//MARK: - Loading + +public extension SceneCoordinator { + func showLoading() { + guard let rootViewController else { return } + + MBProgressHUD.showAdded(to: rootViewController.view, animated: true) + } + + @MainActor + func hideLoading() { + guard let rootViewController else { return } + + MBProgressHUD.hide(for: rootViewController.view, animated: true) + } +} + //MARK: - MastodonLoginViewControllerDelegate extension SceneCoordinator: MastodonLoginViewControllerDelegate { From d15181dcb6c935af1b0118e4aa8a958a2a4557d3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 23 Oct 2023 13:57:50 +0200 Subject: [PATCH 14/24] Show profile on tap Bridge account to user as long as Profile-screen doesn't work with Mastodon.Entity.Account, but MastodonUser --- .../Provider/DataSourceFacade+Profile.swift | 27 ++++++++++++++++++- .../DataSourceFacade+SearchHistory.swift | 3 ++- ...taSourceProvider+UITableViewDelegate.swift | 2 ++ .../Provider/DataSourceProvider.swift | 1 + ...istViewController+DataSourceProvider.swift | 2 ++ ...ultViewController+DataSourceProvider.swift | 3 +++ .../Entity/Mastodon+Entity+Account.swift | 8 ++++-- 7 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 8f77a1888..30c024f54 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -8,6 +8,7 @@ import UIKit import CoreDataStack import MastodonCore +import MastodonSDK extension DataSourceFacade { @@ -53,7 +54,31 @@ extension DataSourceFacade { transition: .show ) } - + + @MainActor + static func coordinateToProfileScene( + provider: ViewControllerWithDependencies & AuthContextProvider, + account: Mastodon.Entity.Account + ) async { + provider.coordinator.showLoading() + + guard let domain = account.domain else { return provider.coordinator.hideLoading() } + + Task { + do { + let user = try await provider.context.apiService.fetchUser(username: account.username, + domain: domain, + authenticationBox: provider.authContext.mastodonAuthenticationBox) + provider.coordinator.hideLoading() + + if let user { + await coordinateToProfileScene(provider: provider, user: user.asRecord) + } + } catch { + provider.coordinator.hideLoading() + } + } + } } extension DataSourceFacade { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index edc4fbe2f..31206c262 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -17,7 +17,8 @@ extension DataSourceFacade { item: DataSourceItem ) async { switch item { - case .status: + + case .status, .account(_, _): break // not create search history for status case .user(let record): let authenticationBox = provider.authContext.mastodonAuthenticationBox diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index c45ef4ca1..5143b3c10 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -21,6 +21,8 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid return } switch item { + case .account(let account, relationship: _): + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) case .status(let status): await DataSourceFacade.coordinateToStatusThreadScene( provider: self, diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index 6df47ccae..b92aadcef 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -16,6 +16,7 @@ enum DataSourceItem: Hashable { case user(record: ManagedObjectRecord) case hashtag(tag: TagKind) case notification(record: ManagedObjectRecord) + case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) } extension DataSourceItem { diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift index 3ea2a74c1..790c8e2ac 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift @@ -20,6 +20,8 @@ extension FollowingListViewController: DataSourceProvider { } switch item { + case .account(let account, let relationship): + return .account(account: account, relationship: relationship) case .user(let record): return .user(record: record) default: diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index f2e8c7c6e..5e9d9e2db 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -52,6 +52,9 @@ extension SearchResultViewController { ) switch item { + case .account(account: _, relationship: _): + // do nothing + break case .status(let status): await DataSourceFacade.coordinateToStatusThreadScene( provider: self, diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 34fdad5ba..eb2910bb9 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -91,11 +91,15 @@ extension Mastodon.Entity.Account { } return acct } -} -extension Mastodon.Entity.Account { public var verifiedLink: Mastodon.Entity.Field? { let firstVerified = fields?.first(where: { $0.verifiedAt != nil }) return firstVerified } + + public var domain: String? { + guard let components = URLComponents(string: url) else { return nil } + + return components.host + } } From c597ee3039f8de57d91af00dcd001ac204a468c8 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 23 Oct 2023 14:07:05 +0200 Subject: [PATCH 15/24] Also reset relationships --- .../Scene/Profile/Following/FollowingListViewModel+State.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift index 924e9db55..21cdd56ac 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift @@ -56,7 +56,8 @@ extension FollowingListViewModel.State { // reset viewModel.accounts = [] - + viewModel.relationships = [] + stateMachine.enter(Loading.self) } } From ded7972f18df89169de19e36123b8b6504949014 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 24 Oct 2023 12:45:24 +0200 Subject: [PATCH 16/24] Hide follow-button for myself --- Mastodon/Diffable/User/UserSection.swift | 15 +++++++++++++-- .../DataSourceProvider+UITableViewDelegate.swift | 2 +- .../UserTableViewCell+ViewModel.swift | 11 ++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index d636a88b7..6997e5159 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -29,13 +29,24 @@ extension UserSection { tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self)) - return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in + return UITableViewDiffableDataSource(tableView: tableView) { + tableView, + indexPath, + item -> UITableViewCell? in switch item { case .account(let account, let relationship): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell } + cell.userView.setButtonState(.loading) - cell.configure(tableView: tableView, account: account, relationship: relationship, delegate: userTableViewCellDelegate) + cell.configure( + me: me, + tableView: tableView, + account: account, + relationship: relationship, + delegate: userTableViewCellDelegate + ) return cell diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 5143b3c10..0944cee6c 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -14,7 +14,7 @@ import MastodonLocalization extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider { func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true ) + tableView.deselectRow(at: indexPath, animated: true) Task { let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath) guard let item = await item(from: source) else { diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 0fe470914..6c0541c2a 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -32,18 +32,20 @@ extension UserTableViewCell { extension UserTableViewCell { func configure( - me: MastodonUser? = nil, + me: MastodonUser, tableView: UITableView, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, delegate: UserTableViewCellDelegate? ) { - //TODO: Implement userView.configure(with: account) let buttonState: UserView.ButtonState if let relationship { - if relationship.following { + let isMe = account.id == me.id + if isMe { + buttonState = .none + } else if relationship.following { buttonState = .unfollow } else if relationship.blocking || (relationship.domainBlocking ?? false) { buttonState = .blocked @@ -57,10 +59,9 @@ extension UserTableViewCell { } userView.setButtonState(buttonState) + } - - //TODO: Duplicate func configure( me: MastodonUser? = nil, tableView: UITableView, From 02207d1b1f908cd16ceef05b5d1b6f770bcf4f36 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 24 Oct 2023 13:38:41 +0200 Subject: [PATCH 17/24] Slightly refactor account-update For whatever reason, fetchUser and accountInfo returned different results for me (something something ID), that's why I replaced accountInfo which came from #1053 with fetchUser, so the displayed profile is consistent --- Mastodon/Scene/Profile/ProfileViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 1edb7e5f7..09a33e997 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -379,10 +379,10 @@ extension ProfileViewController { .sink { [weak self] (user, _) in guard let self = self, let user = user else { return } Task { - _ = try await self.context.apiService.accountInfo( + _ = try await self.context.apiService.fetchUser( + username: user.username, domain: user.domain, - userID: user.id, - authorization: self.authContext.mastodonAuthenticationBox.userAuthorization + authenticationBox: self.authContext.mastodonAuthenticationBox ) } } From 242f351e104d53852762f80d151db2d114ceb169 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 24 Oct 2023 15:42:23 +0200 Subject: [PATCH 18/24] Update follow-button-state after (un)following a person --- .../View/Content/UserView+Configuration.swift | 5 +- .../UserTableViewCell+ViewModel.swift | 47 +++++++++---------- .../View/Content/UserView+ViewModel.swift | 1 + .../MastodonUI/View/Content/UserView.swift | 29 ++++++++++-- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index d67f2ec67..e3f91f462 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -21,6 +21,7 @@ extension UserView { self.delegate = delegate viewModel.user = user viewModel.account = nil + viewModel.relationship = nil Publishers.CombineLatest( user.publisher(for: \.avatar), @@ -67,9 +68,11 @@ extension UserView { .store(in: &disposeBag) } - func configure(with account: Mastodon.Entity.Account) { + func configure(with account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, delegate: UserViewDelegate?) { viewModel.account = account + viewModel.relationship = relationship viewModel.user = nil + self.delegate = delegate let authorUsername = PlaintextMetaContent(string: "@\(account.username)") authorUsernameLabel.configure(content: authorUsername) diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 6c0541c2a..c96e203f0 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -38,28 +38,12 @@ extension UserTableViewCell { relationship: Mastodon.Entity.Relationship?, delegate: UserTableViewCellDelegate? ) { - userView.configure(with: account) + userView.configure(with: account, relationship: relationship, delegate: delegate) - let buttonState: UserView.ButtonState - if let relationship { - let isMe = account.id == me.id - if isMe { - buttonState = .none - } else if relationship.following { - buttonState = .unfollow - } else if relationship.blocking || (relationship.domainBlocking ?? false) { - buttonState = .blocked - } else if relationship.requested ?? false { - buttonState = .pending - } else { - buttonState = .follow - } - } else { - buttonState = .none - } + let isMe = account.id == me.id + userView.updateButtonState(with: relationship, isMe: isMe) - userView.setButtonState(buttonState) - + self.delegate = delegate } func configure( @@ -117,15 +101,30 @@ extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextPro ) } } - - func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account) { + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: MastodonUser?) { Task { + await MainActor.run { view.setButtonState(.loading) } + try await DataSourceFacade.responseToUserViewButtonAction( dependency: self, - user: user, + user: account, buttonState: state ) + + let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first + + let isMe: Bool + if let me { + isMe = account.id == me.id + } else { + isMe = false + } + + await MainActor.run { + view.viewModel.relationship = relationship + view.updateButtonState(with: relationship, isMe: isMe) + } + } } - } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index c4c2edd9a..f35afb97b 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -28,6 +28,7 @@ extension UserView { @Published public var authorVerifiedLink: String? @Published public var user: MastodonUser? @Published public var account: Mastodon.Entity.Account? + @Published public var relationship: Mastodon.Entity.Relationship? } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index 242f6c44d..6ad31bc75 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -16,7 +16,7 @@ import MastodonSDK public protocol UserViewDelegate: AnyObject { func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) - func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account) + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account, me: MastodonUser?) } public final class UserView: UIView { @@ -258,10 +258,33 @@ public extension UserView { if let user = viewModel.user { delegate?.userView(self, didTapButtonWith: currentButtonState, for: user) } else if let account = viewModel.account { - delegate?.userView(self, didTapButtonWith: currentButtonState, for: account) + delegate?.userView(self, didTapButtonWith: currentButtonState, for: account, me: nil) } } - + + func updateButtonState(with relationship: Mastodon.Entity.Relationship?, isMe: Bool) { + let buttonState: UserView.ButtonState + + if let relationship { + if isMe { + buttonState = .none + } else if relationship.following { + buttonState = .unfollow + } else if relationship.blocking || (relationship.domainBlocking ?? false) { + buttonState = .blocked + } else if relationship.requested ?? false { + buttonState = .pending + } else { + buttonState = .follow + } + } else { + buttonState = .none + } + + setButtonState(buttonState) + + } + func setButtonState(_ state: ButtonState) { currentButtonState = state prepareButtonStateLayout(for: state) From e9c974527791df245bda95ede729432eb63b5fa0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 25 Oct 2023 14:35:51 +0200 Subject: [PATCH 19/24] Add a dirty hack to give the backend enough time to process the relationship-change --- .../View/TableviewCell/UserTableViewCell+ViewModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index c96e203f0..6b8613292 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -111,6 +111,10 @@ extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextPro buttonState: state ) + // this is a dirty hack to give the backend enough time to process the relationship-change + // Otherwise the relationship might still be `pending` + try await Task.sleep(for: .seconds(1)) + let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first let isMe: Bool From 585e1252af6537d3cb6c59287dea4e7db07f7c0e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 25 Oct 2023 14:59:17 +0200 Subject: [PATCH 20/24] Add refresh-control to following-list --- Mastodon/Coordinator/SceneCoordinator.swift | 5 +- .../FollowingListViewController.swift | 67 ++++++++++++------- .../FollowingListViewModel+State.swift | 13 +++- .../Following/FollowingListViewModel.swift | 4 +- 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index f821aea1f..a12d3509f 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -468,9 +468,8 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController case .following(let viewModel): - let _viewController = FollowingListViewController() - _viewController.viewModel = viewModel - viewController = _viewController + let followingListViewController = FollowingListViewController(viewModel: viewModel, coordinator: self, context: appContext) + viewController = followingListViewController case .familiarFollowers(let viewModel): let _viewController = FamiliarFollowersViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index b7fe7e0a2..3d3655189 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -15,45 +15,58 @@ import CoreDataStack final class FollowingListViewController: UIViewController, NeedsDependency { - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + weak var context: AppContext! + weak var coordinator: SceneCoordinator! var disposeBag = Set() - var viewModel: FollowingListViewModel! - - lazy var tableView: UITableView = { - let tableView = UITableView() + var viewModel: FollowingListViewModel + + let refreshControl: UIRefreshControl + let tableView: UITableView + + init(viewModel: FollowingListViewModel, coordinator: SceneCoordinator, context: AppContext) { + + self.context = context + self.coordinator = coordinator + self.viewModel = viewModel + + tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self)) - tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.backgroundColor = .clear - return tableView - }() - - -} -extension FollowingListViewController { - - override func viewDidLoad() { - super.viewDidLoad() - + refreshControl = UIRefreshControl() + tableView.refreshControl = refreshControl + + super.init(nibName: nil, bundle: nil) + title = L10n.Scene.Following.title - + view.backgroundColor = .secondarySystemBackground - - tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) tableView.pinToParent() - tableView.delegate = self + tableView.refreshControl?.addTarget(self, action: #selector(FollowingListViewController.refresh(_:)), for: .valueChanged) + + viewModel.tableView = tableView + + refreshControl.addTarget(self, action: #selector(FollowingListViewController.refresh(_:)), for: .valueChanged) + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() + viewModel.setupDiffableDataSource( tableView: tableView, userTableViewCellDelegate: self ) - + // setup batch fetch viewModel.listBatchFetchViewModel.setup(scrollView: tableView) viewModel.listBatchFetchViewModel.shouldFetch @@ -75,6 +88,8 @@ extension FollowingListViewController { self.viewModel.stateMachine.enter(FollowingListViewModel.State.Reloading.self) } .store(in: &disposeBag) + + tableView.refreshControl = UIRefreshControl() } override func viewWillAppear(_ animated: Bool) { @@ -82,7 +97,13 @@ extension FollowingListViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } - + + //MARK: - Actions + + @objc + func refresh(_ sender: UIRefreshControl) { + viewModel.stateMachine.enter(FollowingListViewModel.State.Reloading.self) + } } // MARK: - AuthContextProvider diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift index 21cdd56ac..1075cb491 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift @@ -92,6 +92,12 @@ extension FollowingListViewModel.State { return false } } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + viewModel?.tableView?.refreshControl?.endRefreshing() + } } class Loading: FollowingListViewModel.State { @@ -135,7 +141,6 @@ extension FollowingListViewModel.State { var hasNewAppend = false - let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox) var accounts = viewModel.accounts @@ -180,5 +185,11 @@ extension FollowingListViewModel.State { return false } } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + viewModel?.tableView?.refreshControl?.endRefreshing() + } } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift index e07d1b08e..e056a8222 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift @@ -27,7 +27,9 @@ final class FollowingListViewModel { @Published var domain: String? @Published var userID: String? - + + var tableView: UITableView? + // output var diffableDataSource: UITableViewDiffableDataSource? private(set) lazy var stateMachine: GKStateMachine = { From cd25f78ec8cfaa5ff3ce320c3fc64f540bb675fc Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 25 Oct 2023 17:54:18 +0200 Subject: [PATCH 21/24] Remove obsolete code --- .../FollowingListViewController+DataSourceProvider.swift | 6 ++---- .../Scene/Profile/Following/FollowingListViewModel.swift | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift index 790c8e2ac..811d9766d 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift @@ -22,10 +22,8 @@ extension FollowingListViewController: DataSourceProvider { switch item { case .account(let account, let relationship): return .account(account: account, relationship: relationship) - case .user(let record): - return .user(record: record) - default: - return nil + default: + return nil } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift index e056a8222..ef619f6d6 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift @@ -7,8 +7,6 @@ import Foundation import Combine -import CoreData -import CoreDataStack import GameplayKit import MastodonCore import MastodonSDK From 38a9268abbdb2157430c344b04e5087780b2d4b1 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 25 Oct 2023 18:16:36 +0200 Subject: [PATCH 22/24] Move provider in the same file --- Mastodon.xcodeproj/project.pbxproj | 4 --- ...istViewController+DataSourceProvider.swift | 34 ------------------- .../FollowingListViewController.swift | 28 +++++++++++++++ 3 files changed, 28 insertions(+), 38 deletions(-) delete mode 100644 Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 7abc2dc80..067137721 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -318,7 +318,6 @@ DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7442799056400455B82 /* HashtagTableViewCell.swift */; }; DB63F74727990B0600455B82 /* DataSourceFacade+Hashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */; }; DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */; }; - DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */; }; DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */; }; DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */; }; DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */; }; @@ -1023,7 +1022,6 @@ DB63F7442799056400455B82 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = ""; }; DB63F74627990B0600455B82 /* DataSourceFacade+Hashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Hashtag.swift"; sourceTree = ""; }; DB63F7482799126300455B82 /* FollowerListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewController+DataSourceProvider.swift"; sourceTree = ""; }; - DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB63F74C27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryUserCollectionViewCell.swift; sourceTree = ""; }; DB63F74E2799405600455B82 /* SearchHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHistoryViewModel+Diffable.swift"; sourceTree = ""; }; DB63F751279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistorySectionHeaderCollectionReusableView.swift; sourceTree = ""; }; @@ -2320,7 +2318,6 @@ isa = PBXGroup; children = ( DB5B7294273112B100081888 /* FollowingListViewController.swift */, - DB63F74A279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift */, DB5B7297273112C800081888 /* FollowingListViewModel.swift */, DB5B729B273113C200081888 /* FollowingListViewModel+Diffable.swift */, DB5B729D273113F300081888 /* FollowingListViewModel+State.swift */, @@ -3969,7 +3966,6 @@ DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */, DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */, DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */, - DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */, DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */, DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */, D8F917122A4C6B67008A5370 /* GeneralSettingsViewController.swift in Sources */, diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift deleted file mode 100644 index 811d9766d..000000000 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController+DataSourceProvider.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// FollowingListViewController+DataSourceProvider.swift -// Mastodon -// -// Created by MainasuK on 2022-1-20. -// - -import UIKit - -extension FollowingListViewController: DataSourceProvider { - func item(from source: DataSourceItem.Source) async -> DataSourceItem? { - var _indexPath = source.indexPath - if _indexPath == nil, let cell = source.tableViewCell { - _indexPath = await self.indexPath(for: cell) - } - guard let indexPath = _indexPath else { return nil } - - guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { - return nil - } - - switch item { - case .account(let account, let relationship): - return .account(account: account, relationship: relationship) - default: - return nil - } - } - - @MainActor - private func indexPath(for cell: UITableViewCell) async -> IndexPath? { - return tableView.indexPath(for: cell) - } -} diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index 3d3655189..8c6b78dac 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -126,3 +126,31 @@ extension FollowingListViewController: UITableViewDelegate, AutoGenerateTableVie // MARK: - UserTableViewCellDelegate extension FollowingListViewController: UserTableViewCellDelegate {} + + +// MARK: - DataSourceProvider +extension FollowingListViewController: DataSourceProvider { + func item(from source: DataSourceItem.Source) async -> DataSourceItem? { + var _indexPath = source.indexPath + if _indexPath == nil, let cell = source.tableViewCell { + _indexPath = await self.indexPath(for: cell) + } + guard let indexPath = _indexPath else { return nil } + + guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { + return nil + } + + switch item { + case .account(let account, let relationship): + return .account(account: account, relationship: relationship) + default: + return nil + } + } + + @MainActor + private func indexPath(for cell: UITableViewCell) async -> IndexPath? { + return tableView.indexPath(for: cell) + } +} From 3fc27936d6fd7e967f2b2f9276e05c4149df2f1e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 1 Nov 2023 21:28:32 +0100 Subject: [PATCH 23/24] Replace listbatch-model for Following-list with UIScrollViewDelegate-implementation No more recursion due to no timer and better state handling --- .../FollowingListViewController.swift | 27 +++++++++++++++++-- .../FollowingListViewModel+State.swift | 8 ++++++ .../Following/FollowingListViewModel.swift | 5 ++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index 8c6b78dac..ecd26fb34 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -68,11 +68,11 @@ final class FollowingListViewController: UIViewController, NeedsDependency { ) // setup batch fetch - viewModel.listBatchFetchViewModel.setup(scrollView: tableView) - viewModel.listBatchFetchViewModel.shouldFetch + viewModel.shouldFetch .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } + self.viewModel.stateMachine.enter(FollowingListViewModel.State.Loading.self) } .store(in: &disposeBag) @@ -154,3 +154,26 @@ extension FollowingListViewController: DataSourceProvider { return tableView.indexPath(for: cell) } } + +//MARK: - UIScrollViewDelegate + +extension FollowingListViewController: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + + if scrollView.isDragging || scrollView.isTracking { return } + + let frame = scrollView.frame + let contentOffset = scrollView.contentOffset + let contentSize = scrollView.contentSize + + let visibleBottomY = contentOffset.y + frame.height + let offset = 2 * frame.height + let fetchThrottleOffsetY = contentSize.height - offset + + if visibleBottomY > fetchThrottleOffsetY { + viewModel.shouldFetch.send() + } + + } +} + diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift index 1075cb491..ac5913ab1 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift @@ -139,6 +139,14 @@ extension FollowingListViewModel.State { authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) + if accountResponse.value.isEmpty { + await enter(state: NoMore.self) + + viewModel.accounts = [] + viewModel.relationships = [] + return + } + var hasNewAppend = false let newRelationships = try await viewModel.context.apiService.relationship(forAccounts: accountResponse.value, authenticationBox: viewModel.authContext.mastodonAuthenticationBox) diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift index ef619f6d6..247b4fc64 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift @@ -21,11 +21,11 @@ final class FollowingListViewModel { @Published var accounts: [Mastodon.Entity.Account] @Published var relationships: [Mastodon.Entity.Relationship] - let listBatchFetchViewModel: ListBatchFetchViewModel - @Published var domain: String? @Published var userID: String? + let shouldFetch = PassthroughSubject() + var tableView: UITableView? // output @@ -55,6 +55,5 @@ final class FollowingListViewModel { self.userID = userID self.accounts = [] self.relationships = [] - self.listBatchFetchViewModel = ListBatchFetchViewModel() } } From 77bf1ab03d6ca1b23d3ba7383eb78c3aa0f6b752 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 4 Nov 2023 15:11:42 +0100 Subject: [PATCH 24/24] Use correct domain to find users --- .../Scene/Profile/ProfileViewController.swift | 4 ++-- .../Entity/Mastodon/MastodonUser.swift | 1 + .../Service/API/APIService+Account.swift | 16 ++++------------ 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 09a33e997..a1c5e3925 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -377,11 +377,11 @@ extension ProfileViewController { profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear ) .sink { [weak self] (user, _) in - guard let self = self, let user = user else { return } + guard let self, let user else { return } Task { _ = try await self.context.apiService.fetchUser( username: user.username, - domain: user.domain, + domain: user.domainFromAcct, authenticationBox: self.authContext.mastodonAuthenticationBox ) } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 79715794b..83f94fd22 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -8,6 +8,7 @@ import CoreData import Foundation +/// See also `CoreDataStack.MastodonUser`, this extension contains several final public class MastodonUser: NSManagedObject { public typealias ID = String diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 191917476..ad9561565 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -201,8 +201,9 @@ extension APIService { // user let managedObjectContext = self.backgroundManagedObjectContext + var result: MastodonUser? try await managedObjectContext.performChanges { - _ = Persistence.MastodonUser.createOrMerge( + result = Persistence.MastodonUser.createOrMerge( in: managedObjectContext, context: Persistence.MastodonUser.PersistContext( domain: domain, @@ -210,18 +211,9 @@ extension APIService { cache: nil, networkDate: response.networkDate ) - ) - } - var result: MastodonUser? - try await managedObjectContext.perform { - result = Persistence.MastodonUser.fetch(in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: response.value, - cache: nil, - networkDate: response.networkDate - )) + ).user } + return result } }