Implement follow/unfollow/block in UserView (IOS-140)

This commit is contained in:
Marcus Kida 2023-04-25 12:48:53 +02:00
parent 52fb1eff1f
commit e2a05cd747
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
20 changed files with 253 additions and 28 deletions

View File

@ -22,6 +22,7 @@
164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; };
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
27D701F5292FC2D60031BCBB /* DataSourceFacade+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */; };
2A1BF99529F7E68400FA1BA5 /* DataSourceFacade+UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1BF99429F7E68400FA1BA5 /* DataSourceFacade+UserView.swift */; };
2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */; };
2A1FE47E2938C11200784BF1 /* Collection+IsNotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */; };
2A33062D2987DBFA001D4C51 /* FollowersCountHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */; };
@ -613,6 +614,7 @@
164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = "<group>"; };
1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = "<group>"; };
27D701F4292FC2D60031BCBB /* DataSourceFacade+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+URL.swift"; sourceTree = "<group>"; };
2A1BF99429F7E68400FA1BA5 /* DataSourceFacade+UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+UserView.swift"; sourceTree = "<group>"; };
2A1FE47B2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowedTagsViewModel+DiffableDataSource.swift"; sourceTree = "<group>"; };
2A1FE47D2938C11200784BF1 /* Collection+IsNotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+IsNotEmpty.swift"; sourceTree = "<group>"; };
2A33062C2987DBFA001D4C51 /* FollowersCountHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountHistory.swift; sourceTree = "<group>"; };
@ -2390,6 +2392,7 @@
DB63F7532799491600455B82 /* DataSourceFacade+SearchHistory.swift */,
DB159C2A27A17BAC0068DC77 /* DataSourceFacade+Media.swift */,
2AB12E4529362F27006BC925 /* DataSourceFacade+Translate.swift */,
2A1BF99429F7E68400FA1BA5 /* DataSourceFacade+UserView.swift */,
DB697DD5278F4C29004EF2F7 /* DataSourceProvider.swift */,
DB697DDA278F4DE3004EF2F7 /* DataSourceProvider+StatusTableViewCellDelegate.swift */,
DB023D2927A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift */,
@ -3598,6 +3601,7 @@
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
DB0FCB9C27980AB6006C02E2 /* HashtagTimelineViewController+DataSourceProvider.swift in Sources */,
DB63F76F279A7D1100455B82 /* NotificationTableViewCell.swift in Sources */,
2A1BF99529F7E68400FA1BA5 /* DataSourceFacade+UserView.swift in Sources */,
DB0FCB8C2796BF8D006C02E2 /* SearchViewModel+Diffable.swift in Sources */,
DBEFCD76282A143F00C0ABEA /* ReportStatusViewController.swift in Sources */,
DBDFF1952805561700557A48 /* DiscoveryPostsViewModel+Diffable.swift in Sources */,

View File

@ -122,6 +122,7 @@ extension SearchResultSection {
configuration: Configuration
) {
cell.configure(
meUserID: context.authenticationService.mastodonAuthenticationBoxes.first?.userID,
tableView: tableView,
viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate

View File

@ -42,6 +42,7 @@ extension UserSection {
context.managedObjectContext.performAndWait {
guard let user = record.object(in: context.managedObjectContext) else { return }
configure(
context: context,
tableView: tableView,
cell: cell,
viewModel: .init(value: .user(user)),
@ -66,6 +67,7 @@ extension UserSection {
extension UserSection {
static func configure(
context: AppContext,
tableView: UITableView,
cell: UserTableViewCell,
viewModel: UserTableViewCell.ViewModel,
@ -73,6 +75,7 @@ extension UserSection {
) {
cell.configure(
meUserID: context.authenticationService.mastodonAuthenticationBoxes.first?.userID,
tableView: tableView,
viewModel: viewModel,
delegate: configuration.userTableViewCellDelegate

View File

@ -0,0 +1,30 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonUI
import CoreDataStack
import MastodonCore
import MastodonSDK
extension DataSourceFacade {
static func responseToUserViewButtonAction(
dependency: NeedsDependency & AuthContextProvider,
user: ManagedObjectRecord<MastodonUser>,
buttonState: UserView.ButtonState
) async throws {
switch buttonState {
case .follow, .unfollow:
try await DataSourceFacade.responseToUserFollowAction(
dependency: dependency,
user: user
)
case .blocked:
try await DataSourceFacade.responseToUserBlockAction(
dependency: dependency,
user: user
)
case .none:
break //no-op
}
}
}

View File

@ -10,6 +10,8 @@ import UIKit
import Combine
import MastodonCore
import MastodonLocalization
import MastodonUI
import CoreDataStack
final class FamiliarFollowersViewController: UIViewController, NeedsDependency {
@ -91,4 +93,15 @@ extension FamiliarFollowersViewController: UITableViewDelegate, AutoGenerateTabl
}
// MARK: - UserTableViewCellDelegate
extension FamiliarFollowersViewController: UserTableViewCellDelegate { }
extension FamiliarFollowersViewController: UserTableViewCellDelegate {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
Task {
try await DataSourceFacade.responseToUserViewButtonAction(
dependency: self,
user: user.asRecord,
buttonState: state
)
tableView.reloadData()
}
}
}

View File

@ -12,6 +12,7 @@ import Combine
import MastodonCore
import MastodonUI
import MastodonLocalization
import CoreDataStack
final class FollowerListViewController: UIViewController, NeedsDependency {
@ -118,4 +119,15 @@ extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableView
}
// MARK: - UserTableViewCellDelegate
extension FollowerListViewController: UserTableViewCellDelegate { }
extension FollowerListViewController: UserTableViewCellDelegate {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
Task {
try await DataSourceFacade.responseToUserViewButtonAction(
dependency: self,
user: user.asRecord,
buttonState: state
)
tableView.reloadData()
}
}
}

View File

@ -12,6 +12,7 @@ import Combine
import MastodonLocalization
import MastodonCore
import MastodonUI
import CoreDataStack
final class FollowingListViewController: UIViewController, NeedsDependency {
@ -116,4 +117,15 @@ extension FollowingListViewController: UITableViewDelegate, AutoGenerateTableVie
}
// MARK: - UserTableViewCellDelegate
extension FollowingListViewController: UserTableViewCellDelegate { }
extension FollowingListViewController: UserTableViewCellDelegate {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
Task {
try await DataSourceFacade.responseToUserViewButtonAction(
dependency: self,
user: user.asRecord,
buttonState: state
)
tableView.reloadData()
}
}
}

View File

@ -11,6 +11,8 @@ import GameplayKit
import Combine
import MastodonCore
import MastodonLocalization
import MastodonUI
import CoreDataStack
final class FavoritedByViewController: UIViewController, NeedsDependency {
@ -107,4 +109,15 @@ extension FavoritedByViewController: UITableViewDelegate, AutoGenerateTableViewD
}
// MARK: - UserTableViewCellDelegate
extension FavoritedByViewController: UserTableViewCellDelegate { }
extension FavoritedByViewController: UserTableViewCellDelegate {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
Task {
try await DataSourceFacade.responseToUserViewButtonAction(
dependency: self,
user: user.asRecord,
buttonState: state
)
tableView.reloadData()
}
}
}

View File

@ -11,6 +11,8 @@ import GameplayKit
import Combine
import MastodonCore
import MastodonLocalization
import MastodonUI
import CoreDataStack
final class RebloggedByViewController: UIViewController, NeedsDependency {
@ -107,4 +109,15 @@ extension RebloggedByViewController: UITableViewDelegate, AutoGenerateTableViewD
}
// MARK: - UserTableViewCellDelegate
extension RebloggedByViewController: UserTableViewCellDelegate { }
extension RebloggedByViewController: UserTableViewCellDelegate {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) {
Task {
try await DataSourceFacade.responseToUserViewButtonAction(
dependency: self,
user: user.asRecord,
buttonState: state
)
tableView.reloadData()
}
}
}

View File

@ -53,7 +53,6 @@ final class UserListViewModel {
)
// end init
}
}
extension UserListViewModel {

View File

@ -22,6 +22,6 @@ extension SearchHistoryUserCollectionViewCell {
func configure(
viewModel: ViewModel
) {
userView.configure(user: viewModel.value)
userView.configure(user: viewModel.value, delegate: nil)
}
}

View File

@ -15,7 +15,10 @@ import MastodonCore
import Meta
extension UserView {
public func configure(user: MastodonUser) {
public func configure(user: MastodonUser, delegate: UserViewDelegate?) {
self.delegate = delegate
viewModel.user = user
Publishers.CombineLatest(
user.publisher(for: \.avatar),
UserDefaults.shared.publisher(for: \.preferredStaticAvatar)

View File

@ -26,13 +26,25 @@ extension UserTableViewCell {
extension UserTableViewCell {
func configure(
meUserID: MastodonUser.ID?,
tableView: UITableView,
viewModel: ViewModel,
delegate: UserTableViewCellDelegate?
) {
switch viewModel.value {
case .user(let user):
userView.configure(user: user)
userView.configure(user: user, delegate: delegate)
if user.id == meUserID {
userView.setButtonState(.none)
} else if user.blockingBy.contains(where: { $0.id == meUserID }) {
userView.setButtonState(.blocked)
} else if user.followingBy.contains(where: { $0.id == meUserID }) {
userView.setButtonState(.unfollow)
} else {
userView.setButtonState(.follow)
}
}
self.delegate = delegate

View File

@ -13,7 +13,7 @@ import MastodonLocalization
import MastodonUI
import MastodonSDK
protocol UserTableViewCellDelegate: AnyObject { }
protocol UserTableViewCellDelegate: UserViewDelegate, AnyObject { }
final class UserTableViewCell: UITableViewCell {
@ -21,7 +21,9 @@ final class UserTableViewCell: UITableViewCell {
let userView: UserView = {
let view = UserView()
// view.setButtonState(.follow)
[UserView.ButtonState.follow, UserView.ButtonState.unfollow, UserView.ButtonState.blocked].randomElement().map {
view.setButtonState($0)
}
return view
}()

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.150",
"blue" : "0x30",
"green" : "0x3B",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.800",
"green" : "0.227",
"red" : "0.337"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.150",
"blue" : "0.988",
"green" : "0.173",
"red" : "0.337"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -57,6 +57,9 @@ public enum Asset {
public static let inactive = ColorAsset(name: "Colors/Button/inactive")
public static let tagFollow = ColorAsset(name: "Colors/Button/tagFollow")
public static let tagUnfollow = ColorAsset(name: "Colors/Button/tagUnfollow")
public static let userBlocked = ColorAsset(name: "Colors/Button/userBlocked")
public static let userFollow = ColorAsset(name: "Colors/Button/userFollow")
public static let userFollowing = ColorAsset(name: "Colors/Button/userFollowing")
}
public enum Icon {
public static let plus = ColorAsset(name: "Colors/Icon/plus")

View File

@ -5,6 +5,7 @@
// Created by MainasuK on 2022-1-19.
//
import CoreDataStack
import os.log
import UIKit
import Combine
@ -26,6 +27,7 @@ extension UserView {
@Published public var authorUsername: String?
@Published public var authorFollowers: Int?
@Published public var authorVerifiedLink: String?
@Published public var user: MastodonUser?
}
}

View File

@ -9,7 +9,13 @@ import UIKit
import Combine
import MetaTextKit
import MastodonAsset
import MastodonLocalization
import os
import CoreDataStack
public protocol UserViewDelegate: AnyObject {
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser)
}
public final class UserView: UIView {
@ -17,6 +23,10 @@ public final class UserView: UIView {
case none, follow, unfollow, blocked
}
private var currentButtonState: ButtonState = .none
public weak var delegate: UserViewDelegate?
public var disposeBag = Set<AnyCancellable>()
public private(set) lazy var viewModel: ViewModel = {
@ -92,7 +102,6 @@ public final class UserView: UIView {
private let followButton: UIButton = {
let button = FollowButton()
button.cornerRadius = 10
button.setTitle("Follow", for: .normal)
button.isHidden = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setContentCompressionResistancePriority(.required, for: .horizontal)
@ -105,22 +114,7 @@ public final class UserView: UIView {
return button
}()
public func setButtonState(_ state: ButtonState) {
switch state {
case .follow, .unfollow, .blocked:
verifiedStackView.axis = .vertical
verifiedStackView.alignment = .leading
verifiedStackCenterSpacerView.isHidden = true
followButton.isHidden = false
case .none:
verifiedStackView.axis = .horizontal
verifiedStackView.alignment = .leading
verifiedStackCenterSpacerView.isHidden = false
followButton.isHidden = true
}
}
public func prepareForReuse() {
disposeBag.removeAll()
@ -128,6 +122,7 @@ public final class UserView: UIView {
viewModel.authorAvatarImageURL = nil
avatarButton.avatarImageView.cancelTask()
setButtonState(.none)
}
public override init(frame: CGRect) {
@ -246,3 +241,51 @@ private final class FollowButton: RoundedEdgesButton {
}
}
}
public extension UserView {
private func prepareButtonStateLayout(for state: ButtonState) {
switch state {
case .none:
verifiedStackView.axis = .horizontal
verifiedStackView.alignment = .leading
verifiedStackCenterSpacerView.isHidden = false
followButton.isHidden = true
default:
verifiedStackView.axis = .vertical
verifiedStackView.alignment = .leading
verifiedStackCenterSpacerView.isHidden = true
followButton.isHidden = false
}
}
@objc private func didTapButton() {
guard let user = viewModel.user else { return }
delegate?.userView(self, didTapButtonWith: currentButtonState, for: user)
}
func setButtonState(_ state: ButtonState) {
currentButtonState = state
prepareButtonStateLayout(for: state)
switch state {
case .follow:
followButton.setTitle(L10n.Common.Controls.Friendship.follow, for: .normal)
followButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
followButton.setTitleColor(.white, for: .normal)
case .unfollow:
followButton.setTitle(L10n.Common.Controls.Friendship.following, for: .normal)
followButton.setBackgroundColor(Asset.Colors.Button.userFollowing.color, for: .normal)
followButton.setTitleColor(Asset.Colors.Button.userFollow.color, for: .normal)
case .blocked:
followButton.setTitle(L10n.Common.Controls.Friendship.blocked, for: .normal)
followButton.setBackgroundColor(Asset.Colors.Button.userBlocked.color, for: .normal)
followButton.setTitleColor(.systemRed, for: .normal)
case .none:
break
}
followButton.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
followButton.titleLabel?.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15))
}
}