feat: add familiar followers UI component for ProfileCard
This commit is contained in:
parent
945f05703b
commit
531f71b77d
@ -8,6 +8,7 @@
|
|||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import MastodonUI
|
import MastodonUI
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
enum DiscoverySection: CaseIterable {
|
enum DiscoverySection: CaseIterable {
|
||||||
// case posts
|
// case posts
|
||||||
@ -22,9 +23,14 @@ extension DiscoverySection {
|
|||||||
|
|
||||||
class Configuration {
|
class Configuration {
|
||||||
weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate?
|
weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate?
|
||||||
|
let familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher?
|
||||||
|
|
||||||
public init(profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil) {
|
public init(
|
||||||
|
profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil,
|
||||||
|
familiarFollowers: Published<[Mastodon.Entity.FamiliarFollowers]>.Publisher? = nil
|
||||||
|
) {
|
||||||
self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate
|
self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate
|
||||||
|
self.familiarFollowers = familiarFollowers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +63,15 @@ extension DiscoverySection {
|
|||||||
user: user,
|
user: user,
|
||||||
profileCardTableViewCellDelegate: configuration.profileCardTableViewCellDelegate
|
profileCardTableViewCellDelegate: configuration.profileCardTableViewCellDelegate
|
||||||
)
|
)
|
||||||
|
// bind familiarFollowers
|
||||||
|
if let familiarFollowers = configuration.familiarFollowers {
|
||||||
|
familiarFollowers
|
||||||
|
.map { array in array.first(where: { $0.id == user.id }) }
|
||||||
|
.assign(to: \.familiarFollowers, on: cell.profileCardView.viewModel)
|
||||||
|
.store(in: &cell.disposeBag)
|
||||||
|
} else {
|
||||||
|
cell.profileCardView.viewModel.familiarFollowers = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
context.authenticationService.activeMastodonAuthentication
|
context.authenticationService.activeMastodonAuthentication
|
||||||
.map { $0?.user }
|
.map { $0?.user }
|
||||||
|
@ -19,7 +19,8 @@ extension DiscoveryForYouViewModel {
|
|||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
context: context,
|
context: context,
|
||||||
configuration: DiscoverySection.Configuration(
|
configuration: DiscoverySection.Configuration(
|
||||||
profileCardTableViewCellDelegate: profileCardTableViewCellDelegate
|
profileCardTableViewCellDelegate: profileCardTableViewCellDelegate,
|
||||||
|
familiarFollowers: $familiarFollowers
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@ final class DiscoveryForYouViewModel {
|
|||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let userFetchedResultsController: UserFetchedResultsController
|
let userFetchedResultsController: UserFetchedResultsController
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@Published var familiarFollowers: [Mastodon.Entity.FamiliarFollowers] = []
|
||||||
@Published var isFetching = false
|
@Published var isFetching = false
|
||||||
|
|
||||||
// output
|
// output
|
||||||
@ -48,12 +51,35 @@ final class DiscoveryForYouViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension DiscoveryForYouViewModel {
|
extension DiscoveryForYouViewModel {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func fetch() async throws {
|
func fetch() async throws {
|
||||||
guard !isFetching else { return }
|
guard !isFetching else { return }
|
||||||
isFetching = true
|
isFetching = true
|
||||||
defer { isFetching = false }
|
defer { isFetching = false }
|
||||||
|
|
||||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
throw APIService.APIError.implicit(.badRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let userIDs = try await fetchSuggestionAccounts()
|
||||||
|
|
||||||
|
let _familiarFollowersResponse = try? await context.apiService.familiarFollowers(
|
||||||
|
query: .init(ids: userIDs),
|
||||||
|
authenticationBox: authenticationBox
|
||||||
|
)
|
||||||
|
familiarFollowers = _familiarFollowersResponse?.value ?? []
|
||||||
|
userFetchedResultsController.userIDs = userIDs
|
||||||
|
} catch {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchSuggestionAccounts() async throws -> [Mastodon.Entity.Account.ID] {
|
||||||
|
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
throw APIService.APIError.implicit(.badRequest)
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let response = try await context.apiService.suggestionAccountV2(
|
let response = try await context.apiService.suggestionAccountV2(
|
||||||
@ -61,15 +87,15 @@ extension DiscoveryForYouViewModel {
|
|||||||
authenticationBox: authenticationBox
|
authenticationBox: authenticationBox
|
||||||
)
|
)
|
||||||
let userIDs = response.value.map { $0.account.id }
|
let userIDs = response.value.map { $0.account.id }
|
||||||
userFetchedResultsController.userIDs = userIDs
|
return userIDs
|
||||||
} catch {
|
} catch {
|
||||||
// fallback V1
|
// fallback V1
|
||||||
let response2 = try await context.apiService.suggestionAccount(
|
let response = try await context.apiService.suggestionAccount(
|
||||||
query: nil,
|
query: nil,
|
||||||
authenticationBox: authenticationBox
|
authenticationBox: authenticationBox
|
||||||
)
|
)
|
||||||
let userIDs = response2.value.map { $0.id }
|
let userIDs = response.value.map { $0.id }
|
||||||
userFetchedResultsController.userIDs = userIDs
|
return userIDs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ extension APIService {
|
|||||||
func familiarFollowers(
|
func familiarFollowers(
|
||||||
query: Mastodon.API.Account.FamiliarFollowersQuery,
|
query: Mastodon.API.Account.FamiliarFollowersQuery,
|
||||||
authenticationBox: MastodonAuthenticationBox
|
authenticationBox: MastodonAuthenticationBox
|
||||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> {
|
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.FamiliarFollowers]> {
|
||||||
let response = try await Mastodon.API.Account.familiarFollowers(
|
let response = try await Mastodon.API.Account.familiarFollowers(
|
||||||
session: session,
|
session: session,
|
||||||
domain: authenticationBox.domain,
|
domain: authenticationBox.domain,
|
||||||
@ -88,20 +88,23 @@ extension APIService {
|
|||||||
authorization: authenticationBox.userAuthorization
|
authorization: authenticationBox.userAuthorization
|
||||||
).singleOutput()
|
).singleOutput()
|
||||||
|
|
||||||
// let managedObjectContext = backgroundManagedObjectContext
|
let managedObjectContext = backgroundManagedObjectContext
|
||||||
// try await managedObjectContext.performChanges {
|
try await managedObjectContext.performChanges {
|
||||||
// for entity in response.value {
|
for entity in response.value {
|
||||||
// _ = Persistence.MastodonUser.createOrMerge(
|
for account in entity.accounts {
|
||||||
// in: managedObjectContext,
|
_ = Persistence.MastodonUser.createOrMerge(
|
||||||
// context: Persistence.MastodonUser.PersistContext(
|
in: managedObjectContext,
|
||||||
// domain: authenticationBox.domain,
|
context: Persistence.MastodonUser.PersistContext(
|
||||||
// entity: entity.account,
|
domain: authenticationBox.domain,
|
||||||
// cache: nil,
|
entity: account,
|
||||||
// networkDate: response.networkDate
|
cache: nil,
|
||||||
// )
|
networkDate: response.networkDate
|
||||||
// )
|
)
|
||||||
// } // end for … in
|
)
|
||||||
// }
|
|
||||||
|
} // end for account in
|
||||||
|
} // end for entity in
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ extension Mastodon.API.Account {
|
|||||||
domain: String,
|
domain: String,
|
||||||
query: FamiliarFollowersQuery,
|
query: FamiliarFollowersQuery,
|
||||||
authorization: Mastodon.API.OAuth.Authorization
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Account]>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.FamiliarFollowers]>, Error> {
|
||||||
let request = Mastodon.API.get(
|
let request = Mastodon.API.get(
|
||||||
url: familiarFollowersEndpointURL(domain: domain),
|
url: familiarFollowersEndpointURL(domain: domain),
|
||||||
query: query,
|
query: query,
|
||||||
@ -44,24 +44,23 @@ extension Mastodon.API.Account {
|
|||||||
)
|
)
|
||||||
return session.dataTaskPublisher(for: request)
|
return session.dataTaskPublisher(for: request)
|
||||||
.tryMap { data, response in
|
.tryMap { data, response in
|
||||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response)
|
let value = try Mastodon.API.decode(type: [Mastodon.Entity.FamiliarFollowers].self, from: data, response: response)
|
||||||
return Mastodon.Response.Content(value: value, response: response)
|
return Mastodon.Response.Content(value: value, response: response)
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct FamiliarFollowersQuery: GetQuery {
|
public struct FamiliarFollowersQuery: GetQuery {
|
||||||
public let accounts: [Mastodon.Entity.Account.ID]
|
public let ids: [Mastodon.Entity.Account.ID]
|
||||||
|
|
||||||
public init(accounts: [Mastodon.Entity.Account.ID]) {
|
public init(ids: [Mastodon.Entity.Account.ID]) {
|
||||||
self.accounts = accounts
|
self.ids = ids
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryItems: [URLQueryItem]? {
|
var queryItems: [URLQueryItem]? {
|
||||||
var items: [URLQueryItem] = []
|
var items: [URLQueryItem] = []
|
||||||
let accountsValue = accounts.joined(separator: ",")
|
for id in ids {
|
||||||
if !accountsValue.isEmpty {
|
items.append(URLQueryItem(name: "id[]", value: id))
|
||||||
items.append(URLQueryItem(name: "accounts", value: accountsValue))
|
|
||||||
}
|
}
|
||||||
guard !items.isEmpty else { return nil }
|
guard !items.isEmpty else { return nil }
|
||||||
return items
|
return items
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// Mastodon+Entity+FamiliarFollowers.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-5-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Mastodon.Entity {
|
||||||
|
|
||||||
|
/// FamiliarFollowers
|
||||||
|
///
|
||||||
|
/// - Since: 3.5.2
|
||||||
|
/// - Version: 3.5.2
|
||||||
|
/// # Last Update
|
||||||
|
/// 2022/5/16
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](TBD)
|
||||||
|
public class FamiliarFollowers: Codable {
|
||||||
|
public let id: Mastodon.Entity.Account.ID
|
||||||
|
public let accounts: [Mastodon.Entity.Account]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// Mastodon+Entity+Account.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-5-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension Mastodon.Entity.Account {
|
||||||
|
public var displayNameWithFallback: String {
|
||||||
|
if displayName.isEmpty {
|
||||||
|
return username
|
||||||
|
} else {
|
||||||
|
return displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Mastodon.Entity.Account {
|
||||||
|
public func avatarImageURL() -> URL? {
|
||||||
|
let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar
|
||||||
|
return URL(string: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func avatarImageURLWithFallback(domain: String) -> URL {
|
||||||
|
return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")!
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// FamiliarFollowersDashboardView+Configuration.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-5-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension FamiliarFollowersDashboardView {
|
||||||
|
public func configure(familiarFollowers: Mastodon.Entity.FamiliarFollowers?) {
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
|
let accounts = familiarFollowers?.accounts.prefix(4) ?? []
|
||||||
|
|
||||||
|
viewModel.avatarURLs = accounts.map { $0.avatarImageURL() }
|
||||||
|
viewModel.names = accounts.map { $0.displayNameWithFallback }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
//
|
||||||
|
// FamiliarFollowersDashboardView+ViewModel.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-5-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension FamiliarFollowersDashboardView {
|
||||||
|
public final class ViewModel: ObservableObject {
|
||||||
|
public var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "FamiliarFollowersDashboardView", category: "ViewModel")
|
||||||
|
|
||||||
|
@Published var avatarURLs: [URL?] = []
|
||||||
|
@Published var names: [String] = []
|
||||||
|
@Published var backgroundColor: UIColor?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FamiliarFollowersDashboardView.ViewModel {
|
||||||
|
func bind(view: FamiliarFollowersDashboardView) {
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
$avatarURLs,
|
||||||
|
$backgroundColor
|
||||||
|
)
|
||||||
|
.sink { avatarURLs, backgroundColor in
|
||||||
|
view.avatarContainerView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
for (i, avatarURL) in avatarURLs.enumerated() {
|
||||||
|
let avatarButton = AvatarButton()
|
||||||
|
let origin = CGPoint(x: 20 * i, y: 0)
|
||||||
|
let size = CGSize(width: 32, height: 32)
|
||||||
|
avatarButton.size = size
|
||||||
|
avatarButton.frame = CGRect(origin: origin, size: size)
|
||||||
|
view.avatarContainerView.addSubview(avatarButton)
|
||||||
|
avatarButton.avatarImageView.configure(configuration: .init(url: avatarURL))
|
||||||
|
avatarButton.avatarImageView.configure(
|
||||||
|
cornerConfiguration: .init(
|
||||||
|
corner: .fixed(radius: 7),
|
||||||
|
border: .init(
|
||||||
|
color: backgroundColor ?? .clear,
|
||||||
|
width: 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.avatarContainerViewWidthLayoutConstraint.constant = CGFloat(12 + 20 * avatarURLs.count)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
$names
|
||||||
|
.sink { names in
|
||||||
|
// TODO: i18n
|
||||||
|
let description = "Followed by" + names.joined(separator: ", ")
|
||||||
|
view.descriptionLabel.text = description
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// FamiliarFollowersDashboardView.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-5-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonAsset
|
||||||
|
|
||||||
|
public final class FamiliarFollowersDashboardView: UIView {
|
||||||
|
|
||||||
|
let avatarContainerView = UIView()
|
||||||
|
var avatarContainerViewWidthLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
let descriptionLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
|
||||||
|
label.text = "Followed by Pixelflowers, Lee’s Food, and 4 other mutuals"
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.numberOfLines = 0
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
public private(set) lazy var viewModel: ViewModel = {
|
||||||
|
let viewModel = ViewModel()
|
||||||
|
viewModel.bind(view: self)
|
||||||
|
return viewModel
|
||||||
|
}()
|
||||||
|
|
||||||
|
public func prepareForReuse() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FamiliarFollowersDashboardView {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
let stackView = UIStackView()
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.spacing = 8
|
||||||
|
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(stackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
avatarContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.addArrangedSubview(avatarContainerView)
|
||||||
|
avatarContainerViewWidthLayoutConstraint = avatarContainerView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarContainerViewWidthLayoutConstraint,
|
||||||
|
avatarContainerView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1)
|
||||||
|
])
|
||||||
|
stackView.addArrangedSubview(descriptionLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
struct FamiliarFollowersDashboardView_Preview: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UIViewPreview {
|
||||||
|
FamiliarFollowersDashboardView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
@ -18,7 +18,7 @@ extension NotificationView {
|
|||||||
public final class ViewModel: ObservableObject {
|
public final class ViewModel: ObservableObject {
|
||||||
public var disposeBag = Set<AnyCancellable>()
|
public var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
let logger = Logger(subsystem: "StatusView", category: "ViewModel")
|
let logger = Logger(subsystem: "NotificationView", category: "ViewModel")
|
||||||
|
|
||||||
@Published public var userIdentifier: UserIdentifier? // me
|
@Published public var userIdentifier: UserIdentifier? // me
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import Combine
|
|||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import Meta
|
import Meta
|
||||||
import MastodonMeta
|
import MastodonMeta
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
extension ProfileCardView {
|
extension ProfileCardView {
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import AlamofireImage
|
|||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
extension ProfileCardView {
|
extension ProfileCardView {
|
||||||
public class ViewModel: ObservableObject {
|
public class ViewModel: ObservableObject {
|
||||||
@ -44,6 +45,8 @@ extension ProfileCardView {
|
|||||||
|
|
||||||
@Published public var groupedAccessibilityLabel = ""
|
@Published public var groupedAccessibilityLabel = ""
|
||||||
|
|
||||||
|
@Published public var familiarFollowers: Mastodon.Entity.FamiliarFollowers?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
|
backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
|
||||||
Publishers.CombineLatest(
|
Publishers.CombineLatest(
|
||||||
@ -77,6 +80,7 @@ extension ProfileCardView.ViewModel {
|
|||||||
bindBio(view: view)
|
bindBio(view: view)
|
||||||
bindRelationship(view: view)
|
bindRelationship(view: view)
|
||||||
bindDashboard(view: view)
|
bindDashboard(view: view)
|
||||||
|
bindFamiliarFollowers(view: view)
|
||||||
bindAccessibility(view: view)
|
bindAccessibility(view: view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +193,18 @@ extension ProfileCardView.ViewModel {
|
|||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func bindFamiliarFollowers(view: ProfileCardView) {
|
||||||
|
$familiarFollowers
|
||||||
|
.sink { familiarFollowers in
|
||||||
|
view.familiarFollowersDashboardViewAdaptiveMarginContainerView.isHidden = familiarFollowers.flatMap { $0.accounts.isEmpty } ?? true
|
||||||
|
view.familiarFollowersDashboardView.configure(familiarFollowers: familiarFollowers)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
$backgroundColor
|
||||||
|
.assign(to: \.backgroundColor, on: view.familiarFollowersDashboardView.viewModel)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
private func bindAccessibility(view: ProfileCardView) {
|
private func bindAccessibility(view: ProfileCardView) {
|
||||||
let authorAccessibilityLabel = Publishers.CombineLatest(
|
let authorAccessibilityLabel = Publishers.CombineLatest(
|
||||||
$authorName,
|
$authorName,
|
||||||
|
@ -94,6 +94,9 @@ public final class ProfileCardView: UIView {
|
|||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let familiarFollowersDashboardViewAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||||
|
let familiarFollowersDashboardView = FamiliarFollowersDashboardView()
|
||||||
|
|
||||||
public private(set) lazy var viewModel: ViewModel = {
|
public private(set) lazy var viewModel: ViewModel = {
|
||||||
let viewModel = ViewModel()
|
let viewModel = ViewModel()
|
||||||
viewModel.bind(view: self)
|
viewModel.bind(view: self)
|
||||||
@ -126,7 +129,7 @@ extension ProfileCardView {
|
|||||||
bioMetaText.textView.isUserInteractionEnabled = false
|
bioMetaText.textView.isUserInteractionEnabled = false
|
||||||
statusDashboardView.isUserInteractionEnabled = false
|
statusDashboardView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
// container: V - [ bannerContainer | authorContainer | bioMetaText | infoContainer ]
|
// container: V - [ bannerContainer | authorContainer | bioMetaText | infoContainer | familiarFollowersDashboardView ]
|
||||||
container.axis = .vertical
|
container.axis = .vertical
|
||||||
container.spacing = 8
|
container.spacing = 8
|
||||||
container.translatesAutoresizingMaskIntoConstraints = false
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@ -211,7 +214,7 @@ extension ProfileCardView {
|
|||||||
container.addArrangedSubview(bioMetaTextAdaptiveMarginContainerView)
|
container.addArrangedSubview(bioMetaTextAdaptiveMarginContainerView)
|
||||||
container.setCustomSpacing(16, after: bioMetaTextAdaptiveMarginContainerView)
|
container.setCustomSpacing(16, after: bioMetaTextAdaptiveMarginContainerView)
|
||||||
|
|
||||||
// infoContainer: H - [ statusDashboardView | (spacer) | relationshipActionButton ]
|
// infoContainer: H - [ statusDashboardView | (spacer) | relationshipActionButton]
|
||||||
infoContainer.axis = .horizontal
|
infoContainer.axis = .horizontal
|
||||||
infoContainer.spacing = 8
|
infoContainer.spacing = 8
|
||||||
infoContainerAdaptiveMarginContainerView.contentView = infoContainer
|
infoContainerAdaptiveMarginContainerView.contentView = infoContainer
|
||||||
@ -237,6 +240,10 @@ extension ProfileCardView {
|
|||||||
relationshipActionButtonShadowContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: ProfileCardView.friendshipActionButtonSize.width).priority(.required - 1),
|
relationshipActionButtonShadowContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: ProfileCardView.friendshipActionButtonSize.width).priority(.required - 1),
|
||||||
relationshipActionButtonShadowContainer.heightAnchor.constraint(equalToConstant: ProfileCardView.friendshipActionButtonSize.height).priority(.required - 1),
|
relationshipActionButtonShadowContainer.heightAnchor.constraint(equalToConstant: ProfileCardView.friendshipActionButtonSize.height).priority(.required - 1),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
familiarFollowersDashboardViewAdaptiveMarginContainerView.contentView = familiarFollowersDashboardView
|
||||||
|
familiarFollowersDashboardViewAdaptiveMarginContainerView.margin = ProfileCardView.contentMargin
|
||||||
|
container.addArrangedSubview(familiarFollowersDashboardViewAdaptiveMarginContainerView)
|
||||||
|
|
||||||
let bottomPadding = UIView()
|
let bottomPadding = UIView()
|
||||||
bottomPadding.translatesAutoresizingMaskIntoConstraints = false
|
bottomPadding.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -44,6 +44,11 @@ extension AvatarImageView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setup(border: CornerConfiguration.Border?) {
|
||||||
|
layer.borderColor = border?.color.cgColor
|
||||||
|
layer.borderWidth = border?.width ?? .zero
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AvatarImageView {
|
extension AvatarImageView {
|
||||||
@ -107,9 +112,14 @@ extension AvatarImageView {
|
|||||||
extension AvatarImageView {
|
extension AvatarImageView {
|
||||||
public struct CornerConfiguration {
|
public struct CornerConfiguration {
|
||||||
public let corner: Corner
|
public let corner: Corner
|
||||||
|
public let border: Border?
|
||||||
|
|
||||||
public init(corner: Corner = .circle) {
|
public init(
|
||||||
|
corner: Corner = .circle,
|
||||||
|
border: Border? = nil
|
||||||
|
) {
|
||||||
self.corner = corner
|
self.corner = corner
|
||||||
|
self.border = border
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Corner {
|
public enum Corner {
|
||||||
@ -117,10 +127,16 @@ extension AvatarImageView {
|
|||||||
case fixed(radius: CGFloat)
|
case fixed(radius: CGFloat)
|
||||||
case scale(ratio: Int = 4) // width / ratio
|
case scale(ratio: Int = 4) // width / ratio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Border {
|
||||||
|
public let color: UIColor
|
||||||
|
public let width: CGFloat
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func configure(cornerConfiguration: CornerConfiguration) {
|
public func configure(cornerConfiguration: CornerConfiguration) {
|
||||||
self.cornerConfiguration = cornerConfiguration
|
self.cornerConfiguration = cornerConfiguration
|
||||||
setup(corner: cornerConfiguration.corner)
|
setup(corner: cornerConfiguration.corner)
|
||||||
|
setup(border: cornerConfiguration.border)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
extension ProfileCardTableViewCell {
|
extension ProfileCardTableViewCell {
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user