diff --git a/Extensions/UIButton+Extensions.swift b/Extensions/UIButton+Extensions.swift new file mode 100644 index 0000000..1ff628b --- /dev/null +++ b/Extensions/UIButton+Extensions.swift @@ -0,0 +1,14 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import UIKit + +extension UIButton { + func setAttributedLocalizedTitle(localizationKey: String, count: Int) { + let localizedTitle = String.localizedStringWithFormat(NSLocalizedString(localizationKey, comment: ""), count) + + setAttributedTitle(localizedTitle.countEmphasizedAttributedString(count: count), for: .normal) + setAttributedTitle( + localizedTitle.countEmphasizedAttributedString(count: count, highlighted: true), + for: .highlighted) + } +} diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index ea64c80..181a323 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -4,6 +4,7 @@ "account.field.verified" = "Verified %@"; "account.follow" = "Follow"; "account.following" = "Following"; +"account.following-count" = "%ld Following"; "account.hide-reblogs" = "Hide boosts"; "account.mute" = "Mute"; "account.request" = "Request"; diff --git a/MastodonAPI/Sources/MastodonAPI/Endpoints/AccountsEndpoint.swift b/MastodonAPI/Sources/MastodonAPI/Endpoints/AccountsEndpoint.swift index fde02be..95445a3 100644 --- a/MastodonAPI/Sources/MastodonAPI/Endpoints/AccountsEndpoint.swift +++ b/MastodonAPI/Sources/MastodonAPI/Endpoints/AccountsEndpoint.swift @@ -9,6 +9,8 @@ public enum AccountsEndpoint { case favouritedBy(id: Status.Id) case mutes case blocks + case accountsFollowers(id: Account.Id) + case accountsFollowing(id: Account.Id) } extension AccountsEndpoint: Endpoint { @@ -20,6 +22,8 @@ extension AccountsEndpoint: Endpoint { return defaultContext + ["statuses"] case .mutes, .blocks: return defaultContext + case .accountsFollowers, .accountsFollowing: + return defaultContext + ["accounts"] } } @@ -33,6 +37,10 @@ extension AccountsEndpoint: Endpoint { return ["mutes"] case .blocks: return ["blocks"] + case let .accountsFollowers(id): + return [id, "followers"] + case let .accountsFollowing(id): + return [id, "following"] } } diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 81a4c25..d031014 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ D08B8D72254246E200B1EBEF /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D71254246E200B1EBEF /* PollView.swift */; }; D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D812544D80000B1EBEF /* PollOptionButton.swift */; }; D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */; }; + D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */; }; D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; }; D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; }; D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; }; @@ -157,6 +158,7 @@ D08B8D71254246E200B1EBEF /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = ""; }; D08B8D812544D80000B1EBEF /* PollOptionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionButton.swift; sourceTree = ""; }; D08B8D8C2544E6EC00B1EBEF /* PollResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultView.swift; sourceTree = ""; }; + D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extensions.swift"; sourceTree = ""; }; D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = ""; }; D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = ""; }; D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = ""; }; @@ -433,6 +435,7 @@ D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */, D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */, D0C7D46A24F76169001EBDBB /* String+Extensions.swift */, + D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */, D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */, D0030981250C6C8500EACB32 /* URL+Extensions.swift */, D0C7D46F24F76169001EBDBB /* View+Extensions.swift */, @@ -674,6 +677,7 @@ D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */, D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */, D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */, + D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */, D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */, D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */, D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */, diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift index f89ca84..13ad409 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift @@ -90,6 +90,20 @@ public extension AccountService { func report(_ elements: ReportElements) -> AnyPublisher { mastodonAPIClient.request(ReportEndpoint.create(elements)).ignoreOutput().eraseToAnyPublisher() } + + func followingService() -> AccountListService { + AccountListService( + endpoint: .accountsFollowing(id: account.id), + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) + } + + func followersService() -> AccountListService { + AccountListService( + endpoint: .accountsFollowers(id: account.id), + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) + } } private extension AccountService { diff --git a/View Controllers/ProfileViewController.swift b/View Controllers/ProfileViewController.swift index da42bed..019426b 100644 --- a/View Controllers/ProfileViewController.swift +++ b/View Controllers/ProfileViewController.swift @@ -19,7 +19,7 @@ final class ProfileViewController: TableViewController { super.viewDidLoad() // Initial size is to avoid unsatisfiable constraint warning - let accountHeaderView = AccountHeaderView(frame: .init(origin: .zero, size: .init(width: 100, height: 100))) + let accountHeaderView = AccountHeaderView(frame: .init(origin: .zero, size: .init(width: 300, height: 300))) accountHeaderView.viewModel = viewModel diff --git a/ViewModels/Sources/ViewModels/AccountViewModel.swift b/ViewModels/Sources/ViewModels/AccountViewModel.swift index bc26554..ffd0b1b 100644 --- a/ViewModels/Sources/ViewModels/AccountViewModel.swift +++ b/ViewModels/Sources/ViewModels/AccountViewModel.swift @@ -46,6 +46,10 @@ public extension AccountViewModel { var emoji: [Emoji] { accountService.account.emojis } + var followingCount: Int { accountService.account.followingCount } + + var followersCount: Int { accountService.account.followersCount } + var isSelf: Bool { accountService.account.id == identification.identity.account?.id } func avatarURL(profile: Bool = false) -> URL { @@ -66,6 +70,20 @@ public extension AccountViewModel { .eraseToAnyPublisher()) } + func followingSelected() { + eventsSubject.send( + Just(.navigation(.collection(accountService.followingService()))) + .setFailureType(to: Error.self) + .eraseToAnyPublisher()) + } + + func followersSelected() { + eventsSubject.send( + Just(.navigation(.collection(accountService.followersService()))) + .setFailureType(to: Error.self) + .eraseToAnyPublisher()) + } + func reportViewModel() -> ReportViewModel { ReportViewModel(accountService: accountService, identification: identification) } diff --git a/Views/AccountHeaderView.swift b/Views/AccountHeaderView.swift index a9e122b..77a2db7 100644 --- a/Views/AccountHeaderView.swift +++ b/Views/AccountHeaderView.swift @@ -18,6 +18,9 @@ final class AccountHeaderView: UIView { let lockedImageView = UIImageView() let fieldsStackView = UIStackView() let noteTextView = TouchFallthroughTextView() + let followStackView = UIStackView() + let followingButton = UIButton() + let followersButton = UIButton() let segmentedControl = UISegmentedControl() var viewModel: ProfileViewModel? { @@ -100,8 +103,17 @@ final class AccountHeaderView: UIView { mutableNote.resizeAttachments(toLineHeight: noteFont.lineHeight) noteTextView.attributedText = mutableNote noteTextView.isHidden = false + + followingButton.setAttributedLocalizedTitle( + localizationKey: "account.following-count", + count: accountViewModel.followingCount) + followersButton.setAttributedLocalizedTitle( + localizationKey: "account.followers-count", + count: accountViewModel.followersCount) + followStackView.isHidden = false } else { noteTextView.isHidden = true + followStackView.isHidden = true } } } @@ -264,6 +276,19 @@ private extension AccountHeaderView { noteTextView.isScrollEnabled = false noteTextView.delegate = self + baseStackView.addArrangedSubview(followStackView) + followStackView.distribution = .fillEqually + + followingButton.addAction( + UIAction { [weak self] _ in self?.viewModel?.accountViewModel?.followingSelected() }, + for: .touchUpInside) + followStackView.addArrangedSubview(followingButton) + + followersButton.addAction( + UIAction { [weak self] _ in self?.viewModel?.accountViewModel?.followersSelected() }, + for: .touchUpInside) + followStackView.addArrangedSubview(followersButton) + for (index, collection) in ProfileCollection.allCases.enumerated() { segmentedControl.insertSegment( action: UIAction(title: collection.title) { [weak self] _ in diff --git a/Views/Status/StatusView.swift b/Views/Status/StatusView.swift index 31c0032..241fcab 100644 --- a/Views/Status/StatusView.swift +++ b/Views/Status/StatusView.swift @@ -347,13 +347,11 @@ private extension StatusView { let noFavorites = viewModel.favoritesCount == 0 let noInteractions = !isContextParent || (noReblogs && noFavorites) - setAttributedLocalizedTitle( - button: rebloggedByButton, + rebloggedByButton.setAttributedLocalizedTitle( localizationKey: "status.reblogs-count", count: viewModel.reblogsCount) rebloggedByButton.isHidden = noReblogs - setAttributedLocalizedTitle( - button: favoritedByButton, + favoritedByButton.setAttributedLocalizedTitle( localizationKey: "status.favorites-count", count: viewModel.favoritesCount) favoritedByButton.isHidden = noFavorites @@ -421,15 +419,6 @@ private extension StatusView { menuButton.setImage(UIImage(systemName: "ellipsis", withConfiguration: UIImage.SymbolConfiguration(scale: scale)), for: .normal) } - - func setAttributedLocalizedTitle(button: UIButton, localizationKey: String, count: Int) { - let localizedTitle = String.localizedStringWithFormat(NSLocalizedString(localizationKey, comment: ""), count) - - button.setAttributedTitle(localizedTitle.countEmphasizedAttributedString(count: count), for: .normal) - button.setAttributedTitle( - localizedTitle.countEmphasizedAttributedString(count: count, highlighted: true), - for: .highlighted) - } } private extension UIButton {