From 1fabcb41ccf1966afb68e8d9ddcc7e504eb7afe1 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Mon, 25 Jan 2021 22:57:44 -0800 Subject: [PATCH] Follow requests --- DB/Sources/DB/Content/ContentDatabase.swift | 2 +- DB/Sources/DB/Entities/CollectionItem.swift | 10 +- DB/Sources/DB/Entities/Identity.swift | 1 + .../Identity/IdentityDatabase+Migration.swift | 1 + DB/Sources/DB/Identity/IdentityDatabase.swift | 3 +- Extensions/CollectionItem+Extensions.swift | 4 +- Localizations/Localizable.strings | 1 + .../Sources/Mastodon/Entities/Account.swift | 22 +++- .../Endpoints/AccountsEndpoint.swift | 5 +- .../Endpoints/RelationshipEndpoint.swift | 13 +- .../AccountsEndpoint+Extensions.swift | 14 ++ .../Services/AccountListService.swift | 2 +- .../Services/AccountService.swift | 8 ++ .../Services/IdentityService.swift | 5 +- .../MainNavigationViewController.swift | 5 +- .../TimelinesViewController.swift | 9 ++ .../View Models/AccountViewModel.swift | 27 +++- .../CollectionItemsViewModel.swift | 21 +-- .../View Models/NavigationViewModel.swift | 14 ++ Views/AccountView.swift | 123 +++++++++++++----- Views/SecondaryNavigationView.swift | 16 +++ 21 files changed, 243 insertions(+), 63 deletions(-) create mode 100644 ServiceLayer/Sources/ServiceLayer/Extensions/AccountsEndpoint+Extensions.swift diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index 558c370..43dcd66 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -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))) diff --git a/DB/Sources/DB/Entities/CollectionItem.swift b/DB/Sources/DB/Entities/CollectionItem.swift index 9f01b9c..8f8a029 100644 --- a/DB/Sources/DB/Entities/CollectionItem.swift +++ b/DB/Sources/DB/Entities/CollectionItem.swift @@ -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 diff --git a/DB/Sources/DB/Entities/Identity.swift b/DB/Sources/DB/Entities/Identity.swift index 27cd01e..82994d2 100644 --- a/DB/Sources/DB/Entities/Identity.swift +++ b/DB/Sources/DB/Entities/Identity.swift @@ -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 { diff --git a/DB/Sources/DB/Identity/IdentityDatabase+Migration.swift b/DB/Sources/DB/Identity/IdentityDatabase+Migration.swift index c6f6335..5277481 100644 --- a/DB/Sources/DB/Identity/IdentityDatabase+Migration.swift +++ b/DB/Sources/DB/Identity/IdentityDatabase+Migration.swift @@ -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() } } diff --git a/DB/Sources/DB/Identity/IdentityDatabase.swift b/DB/Sources/DB/Identity/IdentityDatabase.swift index c586388..a95568f 100644 --- a/DB/Sources/DB/Identity/IdentityDatabase.swift +++ b/DB/Sources/DB/Identity/IdentityDatabase.swift @@ -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() diff --git a/Extensions/CollectionItem+Extensions.swift b/Extensions/CollectionItem+Extensions.swift index cfbf65c..d37d781 100644 --- a/Extensions/CollectionItem+Extensions.swift +++ b/Extensions/CollectionItem+Extensions.swift @@ -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): diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index a8c18c3..943e414 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -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"; diff --git a/Mastodon/Sources/Mastodon/Entities/Account.swift b/Mastodon/Sources/Mastodon/Entities/Account.swift index efc0467..0d43455 100644 --- a/Mastodon/Sources/Mastodon/Entities/Account.swift +++ b/Mastodon/Sources/Mastodon/Entities/Account.swift @@ -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 { diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/AccountsEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/AccountsEndpoint.swift index 95445a3..646f5a4 100644 --- a/MastodonAPI/Sources/MastodonAPI/Endpoints/AccountsEndpoint.swift +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/AccountsEndpoint.swift @@ -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"] } } diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/RelationshipEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/RelationshipEndpoint.swift index 5fae164..e830eb5 100644 --- a/MastodonAPI/Sources/MastodonAPI/Endpoints/RelationshipEndpoint.swift +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/RelationshipEndpoint.swift @@ -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"] } } diff --git a/ServiceLayer/Sources/ServiceLayer/Extensions/AccountsEndpoint+Extensions.swift b/ServiceLayer/Sources/ServiceLayer/Extensions/AccountsEndpoint+Extensions.swift new file mode 100644 index 0000000..2272ebf --- /dev/null +++ b/ServiceLayer/Sources/ServiceLayer/Extensions/AccountsEndpoint+Extensions.swift @@ -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 + } + } +} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift index 0ad9c68..2a90de1 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift @@ -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) diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift index 4046c65..24afeba 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift @@ -92,6 +92,14 @@ public extension AccountService { relationshipAction(.note(note, id: account.id)) } + func acceptFollowRequest() -> AnyPublisher { + relationshipAction(.acceptFollowRequest(id: account.id)) + } + + func rejectFollowRequest() -> AnyPublisher { + relationshipAction(.rejectFollowRequest(id: account.id)) + } + func report(_ elements: ReportElements) -> AnyPublisher { mastodonAPIClient.request(ReportEndpoint.create(elements)).ignoreOutput().eraseToAnyPublisher() } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift index 91505da..c90390f 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift @@ -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 { diff --git a/View Controllers/MainNavigationViewController.swift b/View Controllers/MainNavigationViewController.swift index ef2c75a..0c9b28d 100644 --- a/View Controllers/MainNavigationViewController.swift +++ b/View Controllers/MainNavigationViewController.swift @@ -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) } diff --git a/View Controllers/TimelinesViewController.swift b/View Controllers/TimelinesViewController.swift index 32d6826..407a6a4 100644 --- a/View Controllers/TimelinesViewController.swift +++ b/View Controllers/TimelinesViewController.swift @@ -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) } } diff --git a/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift b/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift index 098f836..1db81fd 100644 --- a/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift @@ -5,9 +5,10 @@ import Foundation import Mastodon import ServiceLayer -public struct AccountViewModel: CollectionItemViewModel { +public final class AccountViewModel: CollectionItemViewModel, ObservableObject { public let events: AnyPublisher, Never> public let identityContext: IdentityContext + public internal(set) var configuration = CollectionItem.AccountConfiguration.withNote private let accountService: AccountService private let eventsSubject = PassthroughSubject, Never>() @@ -138,6 +139,30 @@ public extension AccountViewModel { ignorableOutputEvent(accountService.set(note: note)) } + func acceptFollowRequest() { + ignorableOutputEvent( + accountService.acceptFollowRequest() + .collect() + .flatMap { [weak self] _ -> AnyPublisher 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 in + guard let self = self else { return Empty().eraseToAnyPublisher() } + + return self.identityContext.service.verifyCredentials() + } + .eraseToAnyPublisher()) + } + func domainBlock() { ignorableOutputEvent(accountService.domainBlock()) } diff --git a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift index c778aab..5238c23 100644 --- a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift @@ -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): diff --git a/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift b/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift index 0c76b11..d75f481 100644 --- a/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/NavigationViewModel.swift @@ -8,6 +8,7 @@ import ServiceLayer public final class NavigationViewModel: ObservableObject { public let identityContext: IdentityContext public let timelineNavigations: AnyPublisher + public let followRequestNavigations: AnyPublisher @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() + private let followRequestNavigationsSubject = PassthroughSubject() private var cancellables = Set() 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), diff --git a/Views/AccountView.swift b/Views/AccountView.swift index 543bdca..973fb5b 100644 --- a/Views/AccountView.swift +++ b/Views/AccountView.swift @@ -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 } } diff --git a/Views/SecondaryNavigationView.swift b/Views/SecondaryNavigationView.swift index fcf950c..611d634 100644 --- a/Views/SecondaryNavigationView.swift +++ b/Views/SecondaryNavigationView.swift @@ -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(