feat: add discovery endpoint check logic and handle relationship action

This commit is contained in:
CMK 2022-04-15 17:17:39 +08:00
parent b0fca49413
commit f5aaf2737f
13 changed files with 313 additions and 62 deletions

View File

@ -20,7 +20,13 @@ extension DiscoverySection {
static let logger = Logger(subsystem: "DiscoverySection", category: "logic")
struct Configuration { }
class Configuration {
weak var profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate?
public init(profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? = nil) {
self.profileCardTableViewCellDelegate = profileCardTableViewCellDelegate
}
}
static func diffableDataSource(
tableView: UITableView,
@ -52,6 +58,7 @@ extension DiscoverySection {
.map { $0?.user }
.assign(to: \.me, on: cell.profileCardView.viewModel.relationshipViewModel)
.store(in: &cell.disposeBag)
cell.delegate = configuration.profileCardTableViewCellDelegate
return cell
case .bottomLoader:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell

View File

@ -122,12 +122,12 @@ extension DataSourceFacade {
let barButtonItem: UIBarButtonItem?
}
@MainActor
static func createProfileActionMenu(
dependency: NeedsDependency,
user: ManagedObjectRecord<MastodonUser>
) -> UIMenu {
var children: [UIMenuElement] = []
// @MainActor
// static func createProfileActionMenu(
// dependency: NeedsDependency,
// user: ManagedObjectRecord<MastodonUser>
// ) -> UIMenu {
// var children: [UIMenuElement] = []
// let name = mastodonUser.displayNameWithFallback
//
// if let shareUser = shareUser {
@ -339,9 +339,9 @@ extension DataSourceFacade {
// }
// children.append(deleteAction)
// }
return UIMenu(title: "", options: [], children: children)
}
//
// return UIMenu(title: "", options: [], children: children)
// }
static func createActivityViewController(
dependency: NeedsDependency,

View File

@ -29,13 +29,54 @@ public class DiscoveryViewController: TabmanViewController, NeedsDependency {
coordinator: coordinator
)
let buttonBar: TMBar.ButtonBar = {
private(set) lazy var buttonBar: TMBar.ButtonBar = {
let buttonBar = TMBar.ButtonBar()
buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
buttonBar.backgroundView.style = .custom(view: buttonBarBackgroundView)
buttonBar.layout.interButtonSpacing = 0
buttonBar.layout.contentInset = .zero
buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
buttonBar.indicator.weight = .custom(value: 2)
return buttonBar
}()
let buttonBarBackgroundView: UIView = {
let view = UIView()
let barBottomLine = UIView.separatorLine
barBottomLine.backgroundColor = Asset.Colors.Label.secondary.color.withAlphaComponent(0.5)
barBottomLine.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(barBottomLine)
NSLayoutConstraint.activate([
barBottomLine.leadingAnchor.constraint(equalTo: view.leadingAnchor),
barBottomLine.trailingAnchor.constraint(equalTo: view.trailingAnchor),
barBottomLine.bottomAnchor.constraint(equalTo: view.bottomAnchor),
barBottomLine.heightAnchor.constraint(equalToConstant: 2).priority(.required - 1),
])
return view
}()
func customizeButtonBarAppearance() {
// The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors
// Needs trigger update when `userInterfaceStyle` chagnes
let userInterfaceStyle = traitCollection.userInterfaceStyle
buttonBar.buttons.customize { button in
switch userInterfaceStyle {
case .dark:
// Asset.Colors.Label.primary.color
button.selectedTintColor = UIColor(red: 238.0/255.0, green: 238.0/255.0, blue: 238.0/255.0, alpha: 1.0)
// Asset.Colors.Label.secondary.color
button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0)
default:
// Asset.Colors.Label.primary.color
button.selectedTintColor = UIColor(red: 40.0/255.0, green: 44.0/255.0, blue: 55.0/255.0, alpha: 1.0)
// Asset.Colors.Label.secondary.color
button.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6)
}
button.backgroundColor = .clear
button.contentInset = UIEdgeInsets(top: 12, left: 26, bottom: 12, right: 26)
}
}
}
extension DiscoveryViewController {
@ -58,13 +99,21 @@ extension DiscoveryViewController {
dataSource: viewModel,
at: .top
)
updateBarButtonInsets()
customizeButtonBarAppearance()
viewModel.$viewControllers
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.reloadData()
}
.store(in: &disposeBag)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateBarButtonInsets()
customizeButtonBarAppearance()
}
}
@ -72,24 +121,8 @@ extension DiscoveryViewController {
extension DiscoveryViewController {
private func setupAppearance(theme: Theme) {
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
buttonBar.backgroundView.style = .flat(color: theme.systemBackgroundColor)
}
private func updateBarButtonInsets() {
let margin: CGFloat = {
switch traitCollection.userInterfaceIdiom {
case .phone:
return DiscoveryViewController.containerViewMarginForCompactHorizontalSizeClass
default:
return traitCollection.horizontalSizeClass == .regular ?
DiscoveryViewController.containerViewMarginForRegularHorizontalSizeClass :
DiscoveryViewController.containerViewMarginForCompactHorizontalSizeClass
}
}()
buttonBar.layout.contentInset.left = margin
buttonBar.layout.contentInset.right = margin
view.backgroundColor = theme.secondarySystemBackgroundColor
buttonBarBackgroundView.backgroundColor = theme.systemBackgroundColor
}
}

View File

@ -6,11 +6,14 @@
//
import UIKit
import Combine
import Tabman
import Pageboy
final class DiscoveryViewModel {
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
let discoveryPostsViewController: DiscoveryPostsViewController
@ -18,25 +21,7 @@ final class DiscoveryViewModel {
let discoveryNewsViewController: DiscoveryNewsViewController
let discoveryForYouViewController: DiscoveryForYouViewController
// output
let barItems: [TMBarItemable] = {
let items = [
TMBarItem(title: "Posts"),
TMBarItem(title: "Hashtags"),
TMBarItem(title: "News"),
TMBarItem(title: "For You"),
]
return items
}()
var viewControllers: [ScrollViewContainer] {
return [
discoveryPostsViewController,
discoveryHashtagsViewController,
discoveryNewsViewController,
discoveryForYouViewController,
]
}
@Published var viewControllers: [ScrollViewContainer & PageViewController]
init(context: AppContext, coordinator: SceneCoordinator) {
func setupDependency(_ needsDependency: NeedsDependency) {
@ -69,7 +54,35 @@ final class DiscoveryViewModel {
viewController.viewModel = DiscoveryForYouViewModel(context: context)
return viewController
}()
self.viewControllers = [
discoveryPostsViewController,
discoveryHashtagsViewController,
discoveryNewsViewController,
discoveryForYouViewController,
]
// end init
discoveryPostsViewController.viewModel.$isServerSupportEndpoint
.receive(on: DispatchQueue.main)
.sink { [weak self] isServerSupportEndpoint in
guard let self = self else { return }
if !isServerSupportEndpoint {
self.viewControllers.removeAll(where: {
$0 === self.discoveryPostsViewController || $0 === self.discoveryPostsViewController
})
}
}
.store(in: &disposeBag)
discoveryNewsViewController.viewModel.$isServerSupportEndpoint
.receive(on: DispatchQueue.main)
.sink { [weak self] isServerSupportEndpoint in
guard let self = self else { return }
if !isServerSupportEndpoint {
self.viewControllers.removeAll(where: { $0 === self.discoveryNewsViewController })
}
}
.store(in: &disposeBag)
}
}
@ -95,6 +108,49 @@ extension DiscoveryViewModel: PageboyViewControllerDataSource {
// MARK: - TMBarDataSource
extension DiscoveryViewModel: TMBarDataSource {
func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
return barItems[index]
guard !viewControllers.isEmpty, index < viewControllers.count else {
assertionFailure()
return TMBarItem(title: "")
}
return viewControllers[index].tabItem
}
}
protocol PageViewController: UIViewController {
var tabItemTitle: String { get }
var tabItem: TMBarItemable { get }
}
// MARK: - PageViewController
extension DiscoveryPostsViewController: PageViewController {
var tabItemTitle: String { "Posts" }
var tabItem: TMBarItemable {
return TMBarItem(title: tabItemTitle)
}
}
// MARK: - PageViewController
extension DiscoveryHashtagsViewController: PageViewController {
var tabItemTitle: String { "Hashtags" }
var tabItem: TMBarItemable {
return TMBarItem(title: tabItemTitle)
}
}
// MARK: - PageViewController
extension DiscoveryNewsViewController: PageViewController {
var tabItemTitle: String { "News" }
var tabItem: TMBarItemable {
return TMBarItem(title: tabItemTitle)
}
}
// MARK: - PageViewController
extension DiscoveryForYouViewController: PageViewController {
var tabItemTitle: String { "For You" }
var tabItem: TMBarItemable {
return TMBarItem(title: tabItemTitle)
}
}

View File

@ -64,7 +64,8 @@ extension DiscoveryForYouViewController {
tableView.delegate = self
viewModel.setupDiffableDataSource(
tableView: tableView
tableView: tableView,
profileCardTableViewCellDelegate: self
)
tableView.refreshControl = refreshControl
@ -119,9 +120,27 @@ extension DiscoveryForYouViewController: UITableViewDelegate {
}
// MARK: - ProfileCardTableViewCellDelegate
extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
func profileCardTableViewCell(_ cell: ProfileCardTableViewCell, profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return }
Task {
try await DataSourceFacade.responseToUserFollowAction(
dependency: self,
user: record,
authenticationBox: authenticationBox
)
} // end Task
}
}
// MARK: ScrollViewContainer
extension DiscoveryForYouViewController: ScrollViewContainer {
var scrollView: UIScrollView? {
tableView
}
}

View File

@ -7,16 +7,20 @@
import UIKit
import Combine
import MastodonUI
extension DiscoveryForYouViewModel {
func setupDiffableDataSource(
tableView: UITableView
tableView: UITableView,
profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate
) {
diffableDataSource = DiscoverySection.diffableDataSource(
tableView: tableView,
context: context,
configuration: DiscoverySection.Configuration()
configuration: DiscoverySection.Configuration(
profileCardTableViewCellDelegate: profileCardTableViewCellDelegate
)
)
Task {

View File

@ -183,7 +183,13 @@ extension DiscoveryNewsViewModel.State {
viewModel.didLoadLatest.send()
} catch {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch news fail: \(error.localizedDescription)")
await enter(state: Fail.self)
if let error = error as? Mastodon.API.Error, error.httpResponseStatus.code == 404 {
viewModel.isServerSupportEndpoint = false
await enter(state: NoMore.self)
} else {
await enter(state: Fail.self)
}
viewModel.didLoadLatest.send()
}
} // end Task

View File

@ -38,10 +38,15 @@ final class DiscoveryNewsViewModel {
}()
let didLoadLatest = PassthroughSubject<Void, Never>()
@Published var isServerSupportEndpoint = true
init(context: AppContext) {
self.context = context
// end init
Task {
await checkServerEndpoint()
} // end Task
}
deinit {
@ -49,3 +54,21 @@ final class DiscoveryNewsViewModel {
}
}
extension DiscoveryNewsViewModel {
func checkServerEndpoint() async {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do {
_ = try await context.apiService.trendLinks(
domain: authenticationBox.domain,
query: .init(offset: nil, limit: nil)
)
} catch let error as Mastodon.API.Error where error.httpResponseStatus.code == 404 {
isServerSupportEndpoint = false
} catch {
// do nothing
}
}
}

View File

@ -180,9 +180,16 @@ extension DiscoveryPostsViewModel.State {
}
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
viewModel.didLoadLatest.send()
// } catch let error as?
} catch {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch posts fail: \(error.localizedDescription)")
await enter(state: Fail.self)
if let error = error as? Mastodon.API.Error, error.httpResponseStatus.code == 404 {
viewModel.isServerSupportEndpoint = false
await enter(state: NoMore.self)
} else {
await enter(state: Fail.self)
}
viewModel.didLoadLatest.send()
}
} // end Task

View File

@ -38,6 +38,7 @@ final class DiscoveryPostsViewModel {
}()
let didLoadLatest = PassthroughSubject<Void, Never>()
@Published var isServerSupportEndpoint = true
init(context: AppContext) {
self.context = context
@ -52,6 +53,10 @@ final class DiscoveryPostsViewModel {
.map { $0?.domain }
.assign(to: \.value, on: statusFetchedResultsController.domain)
.store(in: &disposeBag)
Task {
await checkServerEndpoint()
} // end Task
}
deinit {
@ -59,3 +64,20 @@ final class DiscoveryPostsViewModel {
}
}
extension DiscoveryPostsViewModel {
func checkServerEndpoint() async {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
do {
_ = try await context.apiService.trendStatuses(
domain: authenticationBox.domain,
query: .init(offset: nil, limit: nil)
)
} catch let error as Mastodon.API.Error where error.httpResponseStatus.code == 404 {
isServerSupportEndpoint = false
} catch {
// do nothing
}
}
}

View File

@ -12,6 +12,7 @@ import Meta
import AlamofireImage
import CoreDataStack
import MastodonLocalization
import MastodonAsset
extension ProfileCardView {
public class ViewModel: ObservableObject {
@ -19,6 +20,9 @@ extension ProfileCardView {
var disposeBag = Set<AnyCancellable>()
public let relationshipViewModel = RelationshipViewModel()
@Published public var userInterfaceStyle: UIUserInterfaceStyle?
@Published public var backgroundColor: UIColor?
// Author
@Published public var authorBannerImageURL: URL?
@ -37,11 +41,35 @@ extension ProfileCardView {
@Published public var isMuting = false
@Published public var isBlocking = false
@Published public var isBlockedBy = false
init() {
backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
Publishers.CombineLatest(
ThemeService.shared.currentTheme,
$userInterfaceStyle
)
.sink { [weak self] theme, userInterfaceStyle in
guard let self = self else { return }
guard let userInterfaceStyle = userInterfaceStyle else { return }
switch userInterfaceStyle {
case .dark:
self.backgroundColor = theme.systemBackgroundColor
case .light, .unspecified:
self.backgroundColor = Asset.Scene.Discovery.profileCardBackground.color
@unknown default:
self.backgroundColor = Asset.Scene.Discovery.profileCardBackground.color
assertionFailure()
// do nothing
}
}
.store(in: &disposeBag)
}
}
}
extension ProfileCardView.ViewModel {
func bind(view: ProfileCardView) {
bindAppearacne(view: view)
bindHeader(view: view)
bindUser(view: view)
bindBio(view: view)
@ -49,6 +77,18 @@ extension ProfileCardView.ViewModel {
bindDashboard(view: view)
}
private func bindAppearacne(view: ProfileCardView) {
userInterfaceStyle = view.traitCollection.userInterfaceStyle
$backgroundColor
.assign(to: \.backgroundColor, on: view.container)
.store(in: &disposeBag)
$backgroundColor
.assign(to: \.backgroundColor, on: view.avatarButtonBackgroundView)
.store(in: &disposeBag)
}
private func bindHeader(view: ProfileCardView) {
$authorBannerImageURL
.sink { url in

View File

@ -5,17 +5,23 @@
// Created by MainasuK on 2022-4-14.
//
import os.log
import UIKit
import Combine
import MetaTextKit
import MastodonAsset
public protocol ProfileCardViewDelegate: AnyObject {
func profileCardView(_ profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
}
public final class ProfileCardView: UIView {
static let avatarSize = CGSize(width: 56, height: 56)
static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
static let contentMargin: CGFloat = 16
weak var delegate: ProfileCardViewDelegate?
private var _disposeBag = Set<AnyCancellable>()
var disposeBag = Set<AnyCancellable>()
@ -31,6 +37,7 @@ public final class ProfileCardView: UIView {
}()
// avatar
public let avatarButtonBackgroundView = UIView()
public let avatarButton = AvatarButton()
// author name
@ -115,7 +122,6 @@ extension ProfileCardView {
statusDashboardView.isUserInteractionEnabled = false
// container: V - [ bannerContainer | authorContainer | bioMetaText | infoContainer ]
container.backgroundColor = Asset.Scene.Discovery.profileCardBackground.color
container.axis = .vertical
container.spacing = 8
container.translatesAutoresizingMaskIntoConstraints = false
@ -171,8 +177,6 @@ extension ProfileCardView {
avatarButton.heightAnchor.constraint(equalToConstant: ProfileCardView.avatarSize.height).priority(.required - 1),
])
let avatarButtonBackgroundView = UIView()
avatarButtonBackgroundView.backgroundColor = Asset.Scene.Discovery.profileCardBackground.color
avatarButtonBackgroundView.layer.masksToBounds = true
avatarButtonBackgroundView.layer.cornerCurve = .continuous
avatarButtonBackgroundView.layer.cornerRadius = 12
@ -230,6 +234,22 @@ extension ProfileCardView {
NSLayoutConstraint.activate([
bottomPadding.heightAnchor.constraint(equalToConstant: 16)
])
relationshipActionButton.addTarget(self, action: #selector(ProfileCardView.relationshipActionButtonDidPressed(_:)), for: .touchUpInside)
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
viewModel.userInterfaceStyle = traitCollection.userInterfaceStyle
}
}
extension ProfileCardView {
@objc private func relationshipActionButtonDidPressed(_ sender: UIButton) {
os_log(.debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
assert(sender === relationshipActionButton)
delegate?.profileCardView(self, relationshipButtonDidPressed: relationshipActionButton)
}
}

View File

@ -8,8 +8,13 @@
import UIKit
import Combine
public protocol ProfileCardTableViewCellDelegate: AnyObject {
func profileCardTableViewCell(_ cell: ProfileCardTableViewCell, profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
}
public final class ProfileCardTableViewCell: UITableViewCell {
public weak var delegate: ProfileCardTableViewCellDelegate?
public var disposeBag = Set<AnyCancellable>()
public let profileCardView: ProfileCardView = {
@ -63,6 +68,15 @@ extension ProfileCardTableViewCell {
profileCardView.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor),
profileCardView.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor),
])
profileCardView.delegate = self
}
}
// MARK: - ProfileCardViewDelegate
extension ProfileCardTableViewCell: ProfileCardViewDelegate {
public func profileCardView(_ profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
delegate?.profileCardTableViewCell(self, profileCardView: profileCardView, relationshipButtonDidPressed: button)
}
}