Follow requests

This commit is contained in:
Justin Mazzocchi 2021-01-25 22:57:44 -08:00
parent 195f2d6a29
commit 1fabcb41cc
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
21 changed files with 243 additions and 63 deletions

View file

@ -524,7 +524,7 @@ public extension ContentDatabase {
accountIds.firstIndex(of: $0.record.id) ?? 0 accountIds.firstIndex(of: $0.record.id) ?? 0
< accountIds.firstIndex(of: $1.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 { if let limit = limit, accounts.count >= limit {
accounts.append(.moreResults(.init(scope: .accounts))) accounts.append(.moreResults(.init(scope: .accounts)))

View file

@ -5,7 +5,7 @@ import Mastodon
public enum CollectionItem: Hashable { public enum CollectionItem: Hashable {
case status(Status, StatusConfiguration) case status(Status, StatusConfiguration)
case loadMore(LoadMore) case loadMore(LoadMore)
case account(Account) case account(Account, AccountConfiguration)
case notification(MastodonNotification, StatusConfiguration?) case notification(MastodonNotification, StatusConfiguration?)
case conversation(Conversation) case conversation(Conversation)
case tag(Tag) case tag(Tag)
@ -38,13 +38,19 @@ public extension CollectionItem {
} }
} }
enum AccountConfiguration: Hashable {
case withNote
case withoutNote
case followRequest
}
var itemId: Id? { var itemId: Id? {
switch self { switch self {
case let .status(status, _): case let .status(status, _):
return status.id return status.id
case .loadMore: case .loadMore:
return nil return nil
case let .account(account): case let .account(account, _):
return account.id return account.id
case let .notification(notification, _): case let .notification(notification, _):
return notification.id return notification.id

View file

@ -37,6 +37,7 @@ public extension Identity {
public let header: URL public let header: URL
public let headerStatic: URL public let headerStatic: URL
public let emojis: [Emoji] public let emojis: [Emoji]
public let followRequestCount: Int
} }
struct Preferences: Codable, Hashable { struct Preferences: Codable, Hashable {

View file

@ -38,6 +38,7 @@ extension IdentityDatabase {
t.column("header", .text).notNull() t.column("header", .text).notNull()
t.column("headerStatic", .text).notNull() t.column("headerStatic", .text).notNull()
t.column("emojis", .blob).notNull() t.column("emojis", .blob).notNull()
t.column("followRequestCount", .integer).notNull()
} }
} }

View file

@ -92,7 +92,8 @@ public extension IdentityDatabase {
avatarStatic: account.avatarStatic, avatarStatic: account.avatarStatic,
header: account.header, header: account.header,
headerStatic: account.headerStatic, headerStatic: account.headerStatic,
emojis: account.emojis) emojis: account.emojis,
followRequestCount: account.source?.followRequestsCount ?? 0)
.save) .save)
.ignoreOutput() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -40,8 +40,8 @@ extension CollectionItem {
identityContext: identityContext, identityContext: identityContext,
status: status, status: status,
configuration: configuration) configuration: configuration)
case let .account(account): case let .account(account, configuration):
return AccountView.estimatedHeight(width: width, account: account) return AccountView.estimatedHeight(width: width, account: account, configuration: configuration)
case .loadMore: case .loadMore:
return LoadMoreView.estimatedHeight return LoadMoreView.estimatedHeight
case let .notification(notification, configuration): case let .notification(notification, configuration):

View file

@ -70,6 +70,7 @@
"emoji.system-group.flags" = "Flags"; "emoji.system-group.flags" = "Flags";
"error" = "Error"; "error" = "Error";
"favorites" = "Favorites"; "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.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
"registration.username" = "Username"; "registration.username" = "Username";
"registration.email" = "Email"; "registration.email" = "Email";

View file

@ -3,12 +3,6 @@
import Foundation import Foundation
public final class Account: Codable, Identifiable { 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 id: Id
public let username: String public let username: String
public let acct: 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 bot: Bool
@DecodableDefault.False public private(set) var discoverable: Bool @DecodableDefault.False public private(set) var discoverable: Bool
public var moved: Account? public var moved: Account?
public var source: Source?
public init(id: Id, public init(id: Id,
username: String, username: String,
@ -75,6 +70,21 @@ public final class Account: Codable, Identifiable {
public extension Account { public extension Account {
typealias Id = String 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 { extension Account: Hashable {

View file

@ -11,6 +11,7 @@ public enum AccountsEndpoint {
case blocks case blocks
case accountsFollowers(id: Account.Id) case accountsFollowers(id: Account.Id)
case accountsFollowing(id: Account.Id) case accountsFollowing(id: Account.Id)
case followRequests
} }
extension AccountsEndpoint: Endpoint { extension AccountsEndpoint: Endpoint {
@ -20,7 +21,7 @@ extension AccountsEndpoint: Endpoint {
switch self { switch self {
case .rebloggedBy, .favouritedBy: case .rebloggedBy, .favouritedBy:
return defaultContext + ["statuses"] return defaultContext + ["statuses"]
case .mutes, .blocks: case .mutes, .blocks, .followRequests:
return defaultContext return defaultContext
case .accountsFollowers, .accountsFollowing: case .accountsFollowers, .accountsFollowing:
return defaultContext + ["accounts"] return defaultContext + ["accounts"]
@ -41,6 +42,8 @@ extension AccountsEndpoint: Endpoint {
return [id, "followers"] return [id, "followers"]
case let .accountsFollowing(id): case let .accountsFollowing(id):
return [id, "following"] return [id, "following"]
case .followRequests:
return ["follow_requests"]
} }
} }

View file

@ -14,13 +14,20 @@ public enum RelationshipEndpoint {
case accountsPin(id: Account.Id) case accountsPin(id: Account.Id)
case accountsUnpin(id: Account.Id) case accountsUnpin(id: Account.Id)
case note(String, id: Account.Id) case note(String, id: Account.Id)
case acceptFollowRequest(id: Account.Id)
case rejectFollowRequest(id: Account.Id)
} }
extension RelationshipEndpoint: Endpoint { extension RelationshipEndpoint: Endpoint {
public typealias ResultType = Relationship public typealias ResultType = Relationship
public var context: [String] { public var context: [String] {
defaultContext + ["accounts"] switch self {
case .acceptFollowRequest, .rejectFollowRequest:
return defaultContext + ["follow_requests"]
default:
return defaultContext + ["accounts"]
}
} }
public var pathComponentsInContext: [String] { public var pathComponentsInContext: [String] {
@ -43,6 +50,10 @@ extension RelationshipEndpoint: Endpoint {
return [id, "unpin"] return [id, "unpin"]
case let .note(_, id): case let .note(_, id):
return [id, "note"] return [id, "note"]
case let .acceptFollowRequest(id):
return [id, "authorize"]
case let .rejectFollowRequest(id):
return [id, "reject"]
} }
} }

View file

@ -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
}
}
}

View file

@ -32,7 +32,7 @@ public struct AccountListService {
return $0 + $1.filter { !presentIds.contains($0.id) } 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() .eraseToAnyPublisher()
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)

View file

@ -92,6 +92,14 @@ public extension AccountService {
relationshipAction(.note(note, id: account.id)) 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> { func report(_ elements: ReportElements) -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(ReportEndpoint.create(elements)).ignoreOutput().eraseToAnyPublisher() mastodonAPIClient.request(ReportEndpoint.create(elements)).ignoreOutput().eraseToAnyPublisher()
} }

View file

@ -242,11 +242,12 @@ public extension IdentityService {
TimelineService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) TimelineService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
} }
func service(accountList: AccountsEndpoint) -> AccountListService { func service(accountList: AccountsEndpoint, titleComponents: [String]? = nil) -> AccountListService {
AccountListService( AccountListService(
endpoint: accountList, endpoint: accountList,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase,
titleComponents: titleComponents)
} }
func exploreService() -> ExploreService { func exploreService() -> ExploreService {

View file

@ -40,8 +40,9 @@ final class MainNavigationViewController: UITabBarController {
} }
.store(in: &cancellables) .store(in: &cancellables)
viewModel.timelineNavigations viewModel.timelineNavigations.map { _ in }
.sink { [weak self] _ in self?.selectedIndex = 0 } .merge(with: viewModel.followRequestNavigations.map { _ in })
.sink { [weak self] in self?.selectedIndex = 0 }
.store(in: &cancellables) .store(in: &cancellables)
} }

View file

@ -82,6 +82,15 @@ final class TimelinesViewController: UIPageViewController {
self.show(vc, sender: self) self.show(vc, sender: self)
} }
.store(in: &cancellables) .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)
} }
} }

View file

@ -5,9 +5,10 @@ import Foundation
import Mastodon import Mastodon
import ServiceLayer import ServiceLayer
public struct AccountViewModel: CollectionItemViewModel { public final class AccountViewModel: CollectionItemViewModel, ObservableObject {
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never> public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
public let identityContext: IdentityContext public let identityContext: IdentityContext
public internal(set) var configuration = CollectionItem.AccountConfiguration.withNote
private let accountService: AccountService private let accountService: AccountService
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>() private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
@ -138,6 +139,30 @@ public extension AccountViewModel {
ignorableOutputEvent(accountService.set(note: note)) 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() { func domainBlock() {
ignorableOutputEvent(accountService.domainBlock()) ignorableOutputEvent(accountService.domainBlock())
} }

View file

@ -138,7 +138,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
case let .loadMore(loadMore): case let .loadMore(loadMore):
lastSelectedLoadMore = loadMore lastSelectedLoadMore = loadMore
(viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loadMore() (viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loadMore()
case let .account(account): case let .account(account, _):
eventsSubject.send( eventsSubject.send(
.navigation(.profile(collectionService .navigation(.profile(collectionService
.navigationService .navigationService
@ -225,16 +225,19 @@ extension CollectionItemsViewModel: CollectionViewModel {
cache(viewModel: viewModel, forItem: item) cache(viewModel: viewModel, forItem: item)
return viewModel return viewModel
case let .account(account): case let .account(account, configuration):
if let cachedViewModel = cachedViewModel { let viewModel: AccountViewModel
return cachedViewModel
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( viewModel.configuration = configuration
accountService: collectionService.navigationService.accountService(account: account),
identityContext: identityContext)
cache(viewModel: viewModel, forItem: item)
return viewModel return viewModel
case let .notification(notification, statusConfiguration): case let .notification(notification, statusConfiguration):

View file

@ -8,6 +8,7 @@ import ServiceLayer
public final class NavigationViewModel: ObservableObject { public final class NavigationViewModel: ObservableObject {
public let identityContext: IdentityContext public let identityContext: IdentityContext
public let timelineNavigations: AnyPublisher<Timeline, Never> public let timelineNavigations: AnyPublisher<Timeline, Never>
public let followRequestNavigations: AnyPublisher<CollectionViewModel, Never>
@Published public private(set) var recentIdentities = [Identity]() @Published public private(set) var recentIdentities = [Identity]()
@Published public var presentingSecondaryNavigation = false @Published public var presentingSecondaryNavigation = false
@ -38,11 +39,13 @@ public final class NavigationViewModel: ObservableObject {
}() }()
private let timelineNavigationsSubject = PassthroughSubject<Timeline, Never>() private let timelineNavigationsSubject = PassthroughSubject<Timeline, Never>()
private let followRequestNavigationsSubject = PassthroughSubject<CollectionViewModel, Never>()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
public init(identityContext: IdentityContext) { public init(identityContext: IdentityContext) {
self.identityContext = identityContext self.identityContext = identityContext
timelineNavigations = timelineNavigationsSubject.eraseToAnyPublisher() timelineNavigations = timelineNavigationsSubject.eraseToAnyPublisher()
followRequestNavigations = followRequestNavigationsSubject.eraseToAnyPublisher()
identityContext.$identity identityContext.$identity
.sink { [weak self] _ in self?.objectWillChange.send() } .sink { [weak self] _ in self?.objectWillChange.send() }
@ -121,6 +124,17 @@ public extension NavigationViewModel {
timelineNavigationsSubject.send(timeline) 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 { func viewModel(timeline: Timeline) -> CollectionItemsViewModel {
CollectionItemsViewModel( CollectionItemsViewModel(
collectionService: identityContext.service.service(timeline: timeline), collectionService: identityContext.service.service(timeline: timeline),

View file

@ -3,12 +3,15 @@
import Kingfisher import Kingfisher
import Mastodon import Mastodon
import UIKit import UIKit
import ViewModels
final class AccountView: UIView { final class AccountView: UIView {
let avatarImageView = AnimatedImageView() let avatarImageView = AnimatedImageView()
let displayNameLabel = UILabel() let displayNameLabel = UILabel()
let accountLabel = UILabel() let accountLabel = UILabel()
let noteTextView = TouchFallthroughTextView() let noteTextView = TouchFallthroughTextView()
let acceptFollowRequestButton = UIButton()
let rejectFollowRequestButton = UIButton()
private var accountConfiguration: AccountContentConfiguration private var accountConfiguration: AccountContentConfiguration
@ -28,12 +31,21 @@ final class AccountView: UIView {
} }
extension AccountView { extension AccountView {
static func estimatedHeight(width: CGFloat, account: Account) -> CGFloat { static func estimatedHeight(width: CGFloat,
.defaultSpacing * 2 account: Account,
+ .compactSpacing * 2 configuration: CollectionItem.AccountConfiguration) -> CGFloat {
var height = CGFloat.defaultSpacing * 2
+ .compactSpacing
+ account.displayName.height(width: width, font: .preferredFont(forTextStyle: .headline)) + account.displayName.height(width: width, font: .preferredFont(forTextStyle: .headline))
+ account.acct.height(width: width, font: .preferredFont(forTextStyle: .subheadline)) + 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() { func initialSetup() {
let stackView = UIStackView() let stackView = UIStackView()
addSubview(avatarImageView)
addSubview(stackView) addSubview(stackView)
avatarImageView.translatesAutoresizingMaskIntoConstraints = false stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = .defaultSpacing
stackView.alignment = .top
stackView.addArrangedSubview(avatarImageView)
avatarImageView.layer.cornerRadius = .avatarDimension / 2 avatarImageView.layer.cornerRadius = .avatarDimension / 2
avatarImageView.clipsToBounds = true avatarImageView.clipsToBounds = true
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical let verticalStackView = UIStackView()
stackView.spacing = .compactSpacing
stackView.addArrangedSubview(displayNameLabel) stackView.addArrangedSubview(verticalStackView)
stackView.addArrangedSubview(accountLabel) verticalStackView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(noteTextView) verticalStackView.axis = .vertical
verticalStackView.spacing = .compactSpacing
verticalStackView.addArrangedSubview(displayNameLabel)
verticalStackView.addArrangedSubview(accountLabel)
verticalStackView.addArrangedSubview(noteTextView)
displayNameLabel.numberOfLines = 0 displayNameLabel.numberOfLines = 0
displayNameLabel.font = .preferredFont(forTextStyle: .headline) displayNameLabel.font = .preferredFont(forTextStyle: .headline)
displayNameLabel.adjustsFontForContentSizeCategory = true displayNameLabel.adjustsFontForContentSizeCategory = true
@ -92,46 +111,82 @@ private extension AccountView {
noteTextView.backgroundColor = .clear noteTextView.backgroundColor = .clear
noteTextView.delegate = self 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([ NSLayoutConstraint.activate([
avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension), avatarImageView.widthAnchor.constraint(equalToConstant: .avatarDimension),
avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension), avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),
avatarImageView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor), acceptFollowRequestButton.widthAnchor.constraint(greaterThanOrEqualToConstant: .avatarDimension),
avatarImageView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), acceptFollowRequestButton.heightAnchor.constraint(greaterThanOrEqualToConstant: .avatarDimension),
avatarImageView.bottomAnchor.constraint(lessThanOrEqualTo: readableContentGuide.bottomAnchor), rejectFollowRequestButton.widthAnchor.constraint(greaterThanOrEqualToConstant: .avatarDimension),
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: .defaultSpacing), rejectFollowRequestButton.heightAnchor.constraint(greaterThanOrEqualToConstant: .avatarDimension),
stackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor), 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() { 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 displayNameLabel.isHidden = true
} else { } 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) mutableDisplayName.resizeAttachments(toLineHeight: displayNameLabel.font.lineHeight)
displayNameLabel.attributedText = mutableDisplayName displayNameLabel.attributedText = mutableDisplayName
} }
accountLabel.text = accountConfiguration.viewModel.accountName accountLabel.text = viewModel.accountName
let noteFont = UIFont.preferredFont(forTextStyle: .callout) if viewModel.configuration == .withNote {
let mutableNote = NSMutableAttributedString(attributedString: accountConfiguration.viewModel.note) let noteFont = UIFont.preferredFont(forTextStyle: .callout)
let noteRange = NSRange(location: 0, length: mutableNote.length) let mutableNote = NSMutableAttributedString(attributedString: viewModel.note)
let noteRange = NSRange(location: 0, length: mutableNote.length)
mutableNote.removeAttribute(.font, range: noteRange) mutableNote.removeAttribute(.font, range: noteRange)
mutableNote.addAttributes( mutableNote.addAttributes(
[.font: noteFont as Any, [.font: noteFont as Any,
.foregroundColor: UIColor.label], .foregroundColor: UIColor.label],
range: noteRange) range: noteRange)
mutableNote.insert(emojis: accountConfiguration.viewModel.emojis, view: noteTextView) mutableNote.insert(emojis: viewModel.emojis, view: noteTextView)
mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight) 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
} }
} }

View file

@ -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 { Section {
NavigationLink( NavigationLink(