mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-21 15:50:59 +00:00
Follow requests
This commit is contained in:
parent
195f2d6a29
commit
1fabcb41cc
21 changed files with 243 additions and 63 deletions
|
@ -524,7 +524,7 @@ public extension ContentDatabase {
|
|||
accountIds.firstIndex(of: $0.record.id) ?? 0
|
||||
< accountIds.firstIndex(of: $1.record.id) ?? 0
|
||||
}
|
||||
.map { CollectionItem.account(.init(info: $0)) }
|
||||
.map { CollectionItem.account(.init(info: $0), .withoutNote) }
|
||||
|
||||
if let limit = limit, accounts.count >= limit {
|
||||
accounts.append(.moreResults(.init(scope: .accounts)))
|
||||
|
|
|
@ -5,7 +5,7 @@ import Mastodon
|
|||
public enum CollectionItem: Hashable {
|
||||
case status(Status, StatusConfiguration)
|
||||
case loadMore(LoadMore)
|
||||
case account(Account)
|
||||
case account(Account, AccountConfiguration)
|
||||
case notification(MastodonNotification, StatusConfiguration?)
|
||||
case conversation(Conversation)
|
||||
case tag(Tag)
|
||||
|
@ -38,13 +38,19 @@ public extension CollectionItem {
|
|||
}
|
||||
}
|
||||
|
||||
enum AccountConfiguration: Hashable {
|
||||
case withNote
|
||||
case withoutNote
|
||||
case followRequest
|
||||
}
|
||||
|
||||
var itemId: Id? {
|
||||
switch self {
|
||||
case let .status(status, _):
|
||||
return status.id
|
||||
case .loadMore:
|
||||
return nil
|
||||
case let .account(account):
|
||||
case let .account(account, _):
|
||||
return account.id
|
||||
case let .notification(notification, _):
|
||||
return notification.id
|
||||
|
|
|
@ -37,6 +37,7 @@ public extension Identity {
|
|||
public let header: URL
|
||||
public let headerStatic: URL
|
||||
public let emojis: [Emoji]
|
||||
public let followRequestCount: Int
|
||||
}
|
||||
|
||||
struct Preferences: Codable, Hashable {
|
||||
|
|
|
@ -38,6 +38,7 @@ extension IdentityDatabase {
|
|||
t.column("header", .text).notNull()
|
||||
t.column("headerStatic", .text).notNull()
|
||||
t.column("emojis", .blob).notNull()
|
||||
t.column("followRequestCount", .integer).notNull()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -92,7 +92,8 @@ public extension IdentityDatabase {
|
|||
avatarStatic: account.avatarStatic,
|
||||
header: account.header,
|
||||
headerStatic: account.headerStatic,
|
||||
emojis: account.emojis)
|
||||
emojis: account.emojis,
|
||||
followRequestCount: account.source?.followRequestsCount ?? 0)
|
||||
.save)
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
|
|
|
@ -40,8 +40,8 @@ extension CollectionItem {
|
|||
identityContext: identityContext,
|
||||
status: status,
|
||||
configuration: configuration)
|
||||
case let .account(account):
|
||||
return AccountView.estimatedHeight(width: width, account: account)
|
||||
case let .account(account, configuration):
|
||||
return AccountView.estimatedHeight(width: width, account: account, configuration: configuration)
|
||||
case .loadMore:
|
||||
return LoadMoreView.estimatedHeight
|
||||
case let .notification(notification, configuration):
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
"emoji.system-group.flags" = "Flags";
|
||||
"error" = "Error";
|
||||
"favorites" = "Favorites";
|
||||
"follow-requests" = "Follow Requests";
|
||||
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
||||
"registration.username" = "Username";
|
||||
"registration.email" = "Email";
|
||||
|
|
|
@ -3,12 +3,6 @@
|
|||
import Foundation
|
||||
|
||||
public final class Account: Codable, Identifiable {
|
||||
public struct Field: Codable, Hashable {
|
||||
public let name: String
|
||||
public let value: HTML
|
||||
public let verifiedAt: Date?
|
||||
}
|
||||
|
||||
public let id: Id
|
||||
public let username: String
|
||||
public let acct: String
|
||||
|
@ -29,6 +23,7 @@ public final class Account: Codable, Identifiable {
|
|||
@DecodableDefault.False public private(set) var bot: Bool
|
||||
@DecodableDefault.False public private(set) var discoverable: Bool
|
||||
public var moved: Account?
|
||||
public var source: Source?
|
||||
|
||||
public init(id: Id,
|
||||
username: String,
|
||||
|
@ -75,6 +70,21 @@ public final class Account: Codable, Identifiable {
|
|||
|
||||
public extension Account {
|
||||
typealias Id = String
|
||||
|
||||
struct Field: Codable, Hashable {
|
||||
public let name: String
|
||||
public let value: HTML
|
||||
public let verifiedAt: Date?
|
||||
}
|
||||
|
||||
struct Source: Codable, Hashable {
|
||||
public let note: String?
|
||||
public let fields: [Field]
|
||||
public let privacy: Status.Visibility?
|
||||
public let sensitive: Bool?
|
||||
public let language: String?
|
||||
public let followRequestsCount: Int?
|
||||
}
|
||||
}
|
||||
|
||||
extension Account: Hashable {
|
||||
|
|
|
@ -11,6 +11,7 @@ public enum AccountsEndpoint {
|
|||
case blocks
|
||||
case accountsFollowers(id: Account.Id)
|
||||
case accountsFollowing(id: Account.Id)
|
||||
case followRequests
|
||||
}
|
||||
|
||||
extension AccountsEndpoint: Endpoint {
|
||||
|
@ -20,7 +21,7 @@ extension AccountsEndpoint: Endpoint {
|
|||
switch self {
|
||||
case .rebloggedBy, .favouritedBy:
|
||||
return defaultContext + ["statuses"]
|
||||
case .mutes, .blocks:
|
||||
case .mutes, .blocks, .followRequests:
|
||||
return defaultContext
|
||||
case .accountsFollowers, .accountsFollowing:
|
||||
return defaultContext + ["accounts"]
|
||||
|
@ -41,6 +42,8 @@ extension AccountsEndpoint: Endpoint {
|
|||
return [id, "followers"]
|
||||
case let .accountsFollowing(id):
|
||||
return [id, "following"]
|
||||
case .followRequests:
|
||||
return ["follow_requests"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,13 +14,20 @@ public enum RelationshipEndpoint {
|
|||
case accountsPin(id: Account.Id)
|
||||
case accountsUnpin(id: Account.Id)
|
||||
case note(String, id: Account.Id)
|
||||
case acceptFollowRequest(id: Account.Id)
|
||||
case rejectFollowRequest(id: Account.Id)
|
||||
}
|
||||
|
||||
extension RelationshipEndpoint: Endpoint {
|
||||
public typealias ResultType = Relationship
|
||||
|
||||
public var context: [String] {
|
||||
defaultContext + ["accounts"]
|
||||
switch self {
|
||||
case .acceptFollowRequest, .rejectFollowRequest:
|
||||
return defaultContext + ["follow_requests"]
|
||||
default:
|
||||
return defaultContext + ["accounts"]
|
||||
}
|
||||
}
|
||||
|
||||
public var pathComponentsInContext: [String] {
|
||||
|
@ -43,6 +50,10 @@ extension RelationshipEndpoint: Endpoint {
|
|||
return [id, "unpin"]
|
||||
case let .note(_, id):
|
||||
return [id, "note"]
|
||||
case let .acceptFollowRequest(id):
|
||||
return [id, "authorize"]
|
||||
case let .rejectFollowRequest(id):
|
||||
return [id, "reject"]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import MastodonAPI
|
||||
|
||||
extension AccountsEndpoint {
|
||||
var configuration: CollectionItem.AccountConfiguration {
|
||||
switch self {
|
||||
case .followRequests:
|
||||
return .followRequest
|
||||
default:
|
||||
return .withNote
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ public struct AccountListService {
|
|||
|
||||
return $0 + $1.filter { !presentIds.contains($0.id) }
|
||||
}
|
||||
.map { [.init(items: $0.map(CollectionItem.account))] }
|
||||
.map { [.init(items: $0.map { CollectionItem.account($0, endpoint.configuration) })] }
|
||||
.eraseToAnyPublisher()
|
||||
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
||||
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
|
|
|
@ -92,6 +92,14 @@ public extension AccountService {
|
|||
relationshipAction(.note(note, id: account.id))
|
||||
}
|
||||
|
||||
func acceptFollowRequest() -> AnyPublisher<Never, Error> {
|
||||
relationshipAction(.acceptFollowRequest(id: account.id))
|
||||
}
|
||||
|
||||
func rejectFollowRequest() -> AnyPublisher<Never, Error> {
|
||||
relationshipAction(.rejectFollowRequest(id: account.id))
|
||||
}
|
||||
|
||||
func report(_ elements: ReportElements) -> AnyPublisher<Never, Error> {
|
||||
mastodonAPIClient.request(ReportEndpoint.create(elements)).ignoreOutput().eraseToAnyPublisher()
|
||||
}
|
||||
|
|
|
@ -242,11 +242,12 @@ public extension IdentityService {
|
|||
TimelineService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
|
||||
func service(accountList: AccountsEndpoint) -> AccountListService {
|
||||
func service(accountList: AccountsEndpoint, titleComponents: [String]? = nil) -> AccountListService {
|
||||
AccountListService(
|
||||
endpoint: accountList,
|
||||
mastodonAPIClient: mastodonAPIClient,
|
||||
contentDatabase: contentDatabase)
|
||||
contentDatabase: contentDatabase,
|
||||
titleComponents: titleComponents)
|
||||
}
|
||||
|
||||
func exploreService() -> ExploreService {
|
||||
|
|
|
@ -40,8 +40,9 @@ final class MainNavigationViewController: UITabBarController {
|
|||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.timelineNavigations
|
||||
.sink { [weak self] _ in self?.selectedIndex = 0 }
|
||||
viewModel.timelineNavigations.map { _ in }
|
||||
.merge(with: viewModel.followRequestNavigations.map { _ in })
|
||||
.sink { [weak self] in self?.selectedIndex = 0 }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,15 @@ final class TimelinesViewController: UIPageViewController {
|
|||
self.show(vc, sender: self)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.followRequestNavigations.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let vc = TableViewController(viewModel: $0, rootViewModel: self.rootViewModel)
|
||||
|
||||
self.show(vc, sender: self)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,10 @@ import Foundation
|
|||
import Mastodon
|
||||
import ServiceLayer
|
||||
|
||||
public struct AccountViewModel: CollectionItemViewModel {
|
||||
public final class AccountViewModel: CollectionItemViewModel, ObservableObject {
|
||||
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||
public let identityContext: IdentityContext
|
||||
public internal(set) var configuration = CollectionItem.AccountConfiguration.withNote
|
||||
|
||||
private let accountService: AccountService
|
||||
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||
|
@ -138,6 +139,30 @@ public extension AccountViewModel {
|
|||
ignorableOutputEvent(accountService.set(note: note))
|
||||
}
|
||||
|
||||
func acceptFollowRequest() {
|
||||
ignorableOutputEvent(
|
||||
accountService.acceptFollowRequest()
|
||||
.collect()
|
||||
.flatMap { [weak self] _ -> AnyPublisher<Never, Error> in
|
||||
guard let self = self else { return Empty().eraseToAnyPublisher() }
|
||||
|
||||
return self.identityContext.service.verifyCredentials()
|
||||
}
|
||||
.eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func rejectFollowRequest() {
|
||||
ignorableOutputEvent(
|
||||
accountService.rejectFollowRequest()
|
||||
.collect()
|
||||
.flatMap { [weak self] _ -> AnyPublisher<Never, Error> in
|
||||
guard let self = self else { return Empty().eraseToAnyPublisher() }
|
||||
|
||||
return self.identityContext.service.verifyCredentials()
|
||||
}
|
||||
.eraseToAnyPublisher())
|
||||
}
|
||||
|
||||
func domainBlock() {
|
||||
ignorableOutputEvent(accountService.domainBlock())
|
||||
}
|
||||
|
|
|
@ -138,7 +138,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
|||
case let .loadMore(loadMore):
|
||||
lastSelectedLoadMore = loadMore
|
||||
(viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loadMore()
|
||||
case let .account(account):
|
||||
case let .account(account, _):
|
||||
eventsSubject.send(
|
||||
.navigation(.profile(collectionService
|
||||
.navigationService
|
||||
|
@ -225,16 +225,19 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
|||
cache(viewModel: viewModel, forItem: item)
|
||||
|
||||
return viewModel
|
||||
case let .account(account):
|
||||
if let cachedViewModel = cachedViewModel {
|
||||
return cachedViewModel
|
||||
case let .account(account, configuration):
|
||||
let viewModel: AccountViewModel
|
||||
|
||||
if let cachedViewModel = cachedViewModel as? AccountViewModel {
|
||||
viewModel = cachedViewModel
|
||||
} else {
|
||||
viewModel = AccountViewModel(
|
||||
accountService: collectionService.navigationService.accountService(account: account),
|
||||
identityContext: identityContext)
|
||||
cache(viewModel: viewModel, forItem: item)
|
||||
}
|
||||
|
||||
let viewModel = AccountViewModel(
|
||||
accountService: collectionService.navigationService.accountService(account: account),
|
||||
identityContext: identityContext)
|
||||
|
||||
cache(viewModel: viewModel, forItem: item)
|
||||
viewModel.configuration = configuration
|
||||
|
||||
return viewModel
|
||||
case let .notification(notification, statusConfiguration):
|
||||
|
|
|
@ -8,6 +8,7 @@ import ServiceLayer
|
|||
public final class NavigationViewModel: ObservableObject {
|
||||
public let identityContext: IdentityContext
|
||||
public let timelineNavigations: AnyPublisher<Timeline, Never>
|
||||
public let followRequestNavigations: AnyPublisher<CollectionViewModel, Never>
|
||||
|
||||
@Published public private(set) var recentIdentities = [Identity]()
|
||||
@Published public var presentingSecondaryNavigation = false
|
||||
|
@ -38,11 +39,13 @@ public final class NavigationViewModel: ObservableObject {
|
|||
}()
|
||||
|
||||
private let timelineNavigationsSubject = PassthroughSubject<Timeline, Never>()
|
||||
private let followRequestNavigationsSubject = PassthroughSubject<CollectionViewModel, Never>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public init(identityContext: IdentityContext) {
|
||||
self.identityContext = identityContext
|
||||
timelineNavigations = timelineNavigationsSubject.eraseToAnyPublisher()
|
||||
followRequestNavigations = followRequestNavigationsSubject.eraseToAnyPublisher()
|
||||
|
||||
identityContext.$identity
|
||||
.sink { [weak self] _ in self?.objectWillChange.send() }
|
||||
|
@ -121,6 +124,17 @@ public extension NavigationViewModel {
|
|||
timelineNavigationsSubject.send(timeline)
|
||||
}
|
||||
|
||||
func navigateToFollowerRequests() {
|
||||
let followRequestsViewModel = CollectionItemsViewModel(
|
||||
collectionService: identityContext.service.service(
|
||||
accountList: .followRequests,
|
||||
titleComponents: ["follow-requests"]),
|
||||
identityContext: identityContext)
|
||||
|
||||
presentingSecondaryNavigation = false
|
||||
followRequestNavigationsSubject.send(followRequestsViewModel)
|
||||
}
|
||||
|
||||
func viewModel(timeline: Timeline) -> CollectionItemsViewModel {
|
||||
CollectionItemsViewModel(
|
||||
collectionService: identityContext.service.service(timeline: timeline),
|
||||
|
|
|
@ -3,12 +3,15 @@
|
|||
import Kingfisher
|
||||
import Mastodon
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
final class AccountView: UIView {
|
||||
let avatarImageView = AnimatedImageView()
|
||||
let displayNameLabel = UILabel()
|
||||
let accountLabel = UILabel()
|
||||
let noteTextView = TouchFallthroughTextView()
|
||||
let acceptFollowRequestButton = UIButton()
|
||||
let rejectFollowRequestButton = UIButton()
|
||||
|
||||
private var accountConfiguration: AccountContentConfiguration
|
||||
|
||||
|
@ -28,12 +31,21 @@ final class AccountView: UIView {
|
|||
}
|
||||
|
||||
extension AccountView {
|
||||
static func estimatedHeight(width: CGFloat, account: Account) -> CGFloat {
|
||||
.defaultSpacing * 2
|
||||
+ .compactSpacing * 2
|
||||
static func estimatedHeight(width: CGFloat,
|
||||
account: Account,
|
||||
configuration: CollectionItem.AccountConfiguration) -> CGFloat {
|
||||
var height = CGFloat.defaultSpacing * 2
|
||||
+ .compactSpacing
|
||||
+ account.displayName.height(width: width, font: .preferredFont(forTextStyle: .headline))
|
||||
+ account.acct.height(width: width, font: .preferredFont(forTextStyle: .subheadline))
|
||||
+ account.note.attributed.string.height(width: width, font: .preferredFont(forTextStyle: .callout))
|
||||
|
||||
if configuration == .withNote {
|
||||
height += .compactSpacing + account.note.attributed.string.height(
|
||||
width: width,
|
||||
font: .preferredFont(forTextStyle: .callout))
|
||||
}
|
||||
|
||||
return max(height, .avatarDimension + .defaultSpacing * 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,17 +83,24 @@ private extension AccountView {
|
|||
func initialSetup() {
|
||||
let stackView = UIStackView()
|
||||
|
||||
addSubview(avatarImageView)
|
||||
addSubview(stackView)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.spacing = .defaultSpacing
|
||||
stackView.alignment = .top
|
||||
|
||||
stackView.addArrangedSubview(avatarImageView)
|
||||
avatarImageView.layer.cornerRadius = .avatarDimension / 2
|
||||
avatarImageView.clipsToBounds = true
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = .compactSpacing
|
||||
stackView.addArrangedSubview(displayNameLabel)
|
||||
stackView.addArrangedSubview(accountLabel)
|
||||
stackView.addArrangedSubview(noteTextView)
|
||||
|
||||
let verticalStackView = UIStackView()
|
||||
|
||||
stackView.addArrangedSubview(verticalStackView)
|
||||
verticalStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
verticalStackView.axis = .vertical
|
||||
verticalStackView.spacing = .compactSpacing
|
||||
verticalStackView.addArrangedSubview(displayNameLabel)
|
||||
verticalStackView.addArrangedSubview(accountLabel)
|
||||
verticalStackView.addArrangedSubview(noteTextView)
|
||||
displayNameLabel.numberOfLines = 0
|
||||
displayNameLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
@ -92,46 +111,82 @@ private extension AccountView {
|
|||
noteTextView.backgroundColor = .clear
|
||||
noteTextView.delegate = self
|
||||
|
||||
let largeTitlePointSize = UIFont.preferredFont(forTextStyle: .largeTitle).pointSize
|
||||
|
||||
stackView.addArrangedSubview(acceptFollowRequestButton)
|
||||
acceptFollowRequestButton.setImage(
|
||||
UIImage(systemName: "checkmark.circle",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: largeTitlePointSize)),
|
||||
for: .normal)
|
||||
acceptFollowRequestButton.setContentHuggingPriority(.required, for: .horizontal)
|
||||
acceptFollowRequestButton.addAction(
|
||||
UIAction { [weak self] _ in self?.accountConfiguration.viewModel.acceptFollowRequest() },
|
||||
for: .touchUpInside)
|
||||
|
||||
stackView.addArrangedSubview(rejectFollowRequestButton)
|
||||
rejectFollowRequestButton.setImage(
|
||||
UIImage(systemName: "xmark.circle",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: largeTitlePointSize)),
|
||||
for: .normal)
|
||||
rejectFollowRequestButton.tintColor = .systemRed
|
||||
rejectFollowRequestButton.setContentHuggingPriority(.required, for: .horizontal)
|
||||
rejectFollowRequestButton.addAction(
|
||||
UIAction { [weak self] _ in self?.accountConfiguration.viewModel.rejectFollowRequest() },
|
||||
for: .touchUpInside)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension),
|
||||
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
|
||||
avatarImageView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||
avatarImageView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
avatarImageView.bottomAnchor.constraint(lessThanOrEqualTo: readableContentGuide.bottomAnchor),
|
||||
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing),
|
||||
acceptFollowRequestButton.widthAnchor.constraint(greaterThanOrEqualToConstant: .avatarDimension),
|
||||
acceptFollowRequestButton.heightAnchor.constraint(greaterThanOrEqualToConstant: .avatarDimension),
|
||||
rejectFollowRequestButton.widthAnchor.constraint(greaterThanOrEqualToConstant: .avatarDimension),
|
||||
rejectFollowRequestButton.heightAnchor.constraint(greaterThanOrEqualToConstant: .avatarDimension),
|
||||
stackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor)
|
||||
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
func applyAccountConfiguration() {
|
||||
avatarImageView.kf.setImage(with: accountConfiguration.viewModel.avatarURL(profile: false))
|
||||
let viewModel = accountConfiguration.viewModel
|
||||
|
||||
if accountConfiguration.viewModel.displayName.isEmpty {
|
||||
avatarImageView.kf.setImage(with: viewModel.avatarURL(profile: false))
|
||||
|
||||
if viewModel.displayName.isEmpty {
|
||||
displayNameLabel.isHidden = true
|
||||
} else {
|
||||
let mutableDisplayName = NSMutableAttributedString(string: accountConfiguration.viewModel.displayName)
|
||||
let mutableDisplayName = NSMutableAttributedString(string: viewModel.displayName)
|
||||
|
||||
mutableDisplayName.insert(emojis: accountConfiguration.viewModel.emojis, view: displayNameLabel)
|
||||
mutableDisplayName.insert(emojis: viewModel.emojis, view: displayNameLabel)
|
||||
mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
|
||||
displayNameLabel.attributedText = mutableDisplayName
|
||||
}
|
||||
|
||||
accountLabel.text = accountConfiguration.viewModel.accountName
|
||||
accountLabel.text = viewModel.accountName
|
||||
|
||||
let noteFont = UIFont.preferredFont(forTextStyle: .callout)
|
||||
let mutableNote = NSMutableAttributedString(attributedString: accountConfiguration.viewModel.note)
|
||||
let noteRange = NSRange(location: 0, length: mutableNote.length)
|
||||
if viewModel.configuration == .withNote {
|
||||
let noteFont = UIFont.preferredFont(forTextStyle: .callout)
|
||||
let mutableNote = NSMutableAttributedString(attributedString: viewModel.note)
|
||||
let noteRange = NSRange(location: 0, length: mutableNote.length)
|
||||
|
||||
mutableNote.removeAttribute(.font, range: noteRange)
|
||||
mutableNote.addAttributes(
|
||||
[.font: noteFont as Any,
|
||||
.foregroundColor: UIColor.label],
|
||||
range: noteRange)
|
||||
mutableNote.insert(emojis: accountConfiguration.viewModel.emojis, view: noteTextView)
|
||||
mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight)
|
||||
mutableNote.removeAttribute(.font, range: noteRange)
|
||||
mutableNote.addAttributes(
|
||||
[.font: noteFont as Any,
|
||||
.foregroundColor: UIColor.label],
|
||||
range: noteRange)
|
||||
mutableNote.insert(emojis: viewModel.emojis, view: noteTextView)
|
||||
mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight)
|
||||
|
||||
noteTextView.attributedText = mutableNote
|
||||
noteTextView.attributedText = mutableNote
|
||||
noteTextView.isHidden = false
|
||||
} else {
|
||||
noteTextView.isHidden = true
|
||||
}
|
||||
|
||||
let isFollowRequest = viewModel.configuration == .followRequest
|
||||
|
||||
acceptFollowRequestButton.isHidden = !isFollowRequest
|
||||
rejectFollowRequestButton.isHidden = !isFollowRequest
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,22 @@ struct SecondaryNavigationView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
if let followRequestCount = viewModel.identityContext.identity.account?.followRequestCount,
|
||||
followRequestCount > 0 {
|
||||
Button {
|
||||
viewModel.navigateToFollowerRequests()
|
||||
} label: {
|
||||
Label {
|
||||
HStack {
|
||||
Text("follow-requests").foregroundColor(.primary)
|
||||
Spacer()
|
||||
Text(verbatim: String(followRequestCount))
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "person.badge.plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Section {
|
||||
NavigationLink(
|
||||
|
|
Loading…
Reference in a new issue