forked from zelo72/mastodon-ios
feat: add discovery endpoint check logic and handle relationship action
This commit is contained in:
parent
b0fca49413
commit
f5aaf2737f
|
@ -20,7 +20,13 @@ extension DiscoverySection {
|
||||||
|
|
||||||
static let logger = Logger(subsystem: "DiscoverySection", category: "logic")
|
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(
|
static func diffableDataSource(
|
||||||
tableView: UITableView,
|
tableView: UITableView,
|
||||||
|
@ -52,6 +58,7 @@ extension DiscoverySection {
|
||||||
.map { $0?.user }
|
.map { $0?.user }
|
||||||
.assign(to: \.me, on: cell.profileCardView.viewModel.relationshipViewModel)
|
.assign(to: \.me, on: cell.profileCardView.viewModel.relationshipViewModel)
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
cell.delegate = configuration.profileCardTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
|
|
|
@ -122,12 +122,12 @@ extension DataSourceFacade {
|
||||||
let barButtonItem: UIBarButtonItem?
|
let barButtonItem: UIBarButtonItem?
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
// @MainActor
|
||||||
static func createProfileActionMenu(
|
// static func createProfileActionMenu(
|
||||||
dependency: NeedsDependency,
|
// dependency: NeedsDependency,
|
||||||
user: ManagedObjectRecord<MastodonUser>
|
// user: ManagedObjectRecord<MastodonUser>
|
||||||
) -> UIMenu {
|
// ) -> UIMenu {
|
||||||
var children: [UIMenuElement] = []
|
// var children: [UIMenuElement] = []
|
||||||
// let name = mastodonUser.displayNameWithFallback
|
// let name = mastodonUser.displayNameWithFallback
|
||||||
//
|
//
|
||||||
// if let shareUser = shareUser {
|
// if let shareUser = shareUser {
|
||||||
|
@ -339,9 +339,9 @@ extension DataSourceFacade {
|
||||||
// }
|
// }
|
||||||
// children.append(deleteAction)
|
// children.append(deleteAction)
|
||||||
// }
|
// }
|
||||||
|
//
|
||||||
return UIMenu(title: "", options: [], children: children)
|
// return UIMenu(title: "", options: [], children: children)
|
||||||
}
|
// }
|
||||||
|
|
||||||
static func createActivityViewController(
|
static func createActivityViewController(
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
|
|
|
@ -29,13 +29,54 @@ public class DiscoveryViewController: TabmanViewController, NeedsDependency {
|
||||||
coordinator: coordinator
|
coordinator: coordinator
|
||||||
)
|
)
|
||||||
|
|
||||||
let buttonBar: TMBar.ButtonBar = {
|
private(set) lazy var buttonBar: TMBar.ButtonBar = {
|
||||||
let 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.layout.contentInset = .zero
|
||||||
|
buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
|
||||||
|
buttonBar.indicator.weight = .custom(value: 2)
|
||||||
return buttonBar
|
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 {
|
extension DiscoveryViewController {
|
||||||
|
@ -58,13 +99,21 @@ extension DiscoveryViewController {
|
||||||
dataSource: viewModel,
|
dataSource: viewModel,
|
||||||
at: .top
|
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?) {
|
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
super.traitCollectionDidChange(previousTraitCollection)
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
updateBarButtonInsets()
|
customizeButtonBarAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -72,24 +121,8 @@ extension DiscoveryViewController {
|
||||||
extension DiscoveryViewController {
|
extension DiscoveryViewController {
|
||||||
|
|
||||||
private func setupAppearance(theme: Theme) {
|
private func setupAppearance(theme: Theme) {
|
||||||
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
view.backgroundColor = theme.secondarySystemBackgroundColor
|
||||||
buttonBar.backgroundView.style = .flat(color: theme.systemBackgroundColor)
|
buttonBarBackgroundView.backgroundColor = 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,14 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Combine
|
||||||
import Tabman
|
import Tabman
|
||||||
import Pageboy
|
import Pageboy
|
||||||
|
|
||||||
final class DiscoveryViewModel {
|
final class DiscoveryViewModel {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let discoveryPostsViewController: DiscoveryPostsViewController
|
let discoveryPostsViewController: DiscoveryPostsViewController
|
||||||
|
@ -18,25 +21,7 @@ final class DiscoveryViewModel {
|
||||||
let discoveryNewsViewController: DiscoveryNewsViewController
|
let discoveryNewsViewController: DiscoveryNewsViewController
|
||||||
let discoveryForYouViewController: DiscoveryForYouViewController
|
let discoveryForYouViewController: DiscoveryForYouViewController
|
||||||
|
|
||||||
// output
|
@Published var viewControllers: [ScrollViewContainer & PageViewController]
|
||||||
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,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
init(context: AppContext, coordinator: SceneCoordinator) {
|
||||||
func setupDependency(_ needsDependency: NeedsDependency) {
|
func setupDependency(_ needsDependency: NeedsDependency) {
|
||||||
|
@ -69,7 +54,35 @@ final class DiscoveryViewModel {
|
||||||
viewController.viewModel = DiscoveryForYouViewModel(context: context)
|
viewController.viewModel = DiscoveryForYouViewModel(context: context)
|
||||||
return viewController
|
return viewController
|
||||||
}()
|
}()
|
||||||
|
self.viewControllers = [
|
||||||
|
discoveryPostsViewController,
|
||||||
|
discoveryHashtagsViewController,
|
||||||
|
discoveryNewsViewController,
|
||||||
|
discoveryForYouViewController,
|
||||||
|
]
|
||||||
// end init
|
// 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
|
// MARK: - TMBarDataSource
|
||||||
extension DiscoveryViewModel: TMBarDataSource {
|
extension DiscoveryViewModel: TMBarDataSource {
|
||||||
func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,8 @@ extension DiscoveryForYouViewController {
|
||||||
|
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
viewModel.setupDiffableDataSource(
|
viewModel.setupDiffableDataSource(
|
||||||
tableView: tableView
|
tableView: tableView,
|
||||||
|
profileCardTableViewCellDelegate: self
|
||||||
)
|
)
|
||||||
|
|
||||||
tableView.refreshControl = refreshControl
|
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
|
// MARK: ScrollViewContainer
|
||||||
extension DiscoveryForYouViewController: ScrollViewContainer {
|
extension DiscoveryForYouViewController: ScrollViewContainer {
|
||||||
var scrollView: UIScrollView? {
|
var scrollView: UIScrollView? {
|
||||||
tableView
|
tableView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,20 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import MastodonUI
|
||||||
|
|
||||||
extension DiscoveryForYouViewModel {
|
extension DiscoveryForYouViewModel {
|
||||||
|
|
||||||
func setupDiffableDataSource(
|
func setupDiffableDataSource(
|
||||||
tableView: UITableView
|
tableView: UITableView,
|
||||||
|
profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate
|
||||||
) {
|
) {
|
||||||
diffableDataSource = DiscoverySection.diffableDataSource(
|
diffableDataSource = DiscoverySection.diffableDataSource(
|
||||||
tableView: tableView,
|
tableView: tableView,
|
||||||
context: context,
|
context: context,
|
||||||
configuration: DiscoverySection.Configuration()
|
configuration: DiscoverySection.Configuration(
|
||||||
|
profileCardTableViewCellDelegate: profileCardTableViewCellDelegate
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
|
|
@ -183,7 +183,13 @@ extension DiscoveryNewsViewModel.State {
|
||||||
viewModel.didLoadLatest.send()
|
viewModel.didLoadLatest.send()
|
||||||
} catch {
|
} catch {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch news fail: \(error.localizedDescription)")
|
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()
|
viewModel.didLoadLatest.send()
|
||||||
}
|
}
|
||||||
} // end Task
|
} // end Task
|
||||||
|
|
|
@ -38,10 +38,15 @@ final class DiscoveryNewsViewModel {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let didLoadLatest = PassthroughSubject<Void, Never>()
|
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||||
|
@Published var isServerSupportEndpoint = true
|
||||||
|
|
||||||
init(context: AppContext) {
|
init(context: AppContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
// end init
|
// end init
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await checkServerEndpoint()
|
||||||
|
} // end Task
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -180,9 +180,16 @@ extension DiscoveryPostsViewModel.State {
|
||||||
}
|
}
|
||||||
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||||
viewModel.didLoadLatest.send()
|
viewModel.didLoadLatest.send()
|
||||||
|
// } catch let error as?
|
||||||
} catch {
|
} catch {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch posts fail: \(error.localizedDescription)")
|
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()
|
viewModel.didLoadLatest.send()
|
||||||
}
|
}
|
||||||
} // end Task
|
} // end Task
|
||||||
|
|
|
@ -38,6 +38,7 @@ final class DiscoveryPostsViewModel {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let didLoadLatest = PassthroughSubject<Void, Never>()
|
let didLoadLatest = PassthroughSubject<Void, Never>()
|
||||||
|
@Published var isServerSupportEndpoint = true
|
||||||
|
|
||||||
init(context: AppContext) {
|
init(context: AppContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
@ -52,6 +53,10 @@ final class DiscoveryPostsViewModel {
|
||||||
.map { $0?.domain }
|
.map { $0?.domain }
|
||||||
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await checkServerEndpoint()
|
||||||
|
} // end Task
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Meta
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
|
import MastodonAsset
|
||||||
|
|
||||||
extension ProfileCardView {
|
extension ProfileCardView {
|
||||||
public class ViewModel: ObservableObject {
|
public class ViewModel: ObservableObject {
|
||||||
|
@ -19,6 +20,9 @@ extension ProfileCardView {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
public let relationshipViewModel = RelationshipViewModel()
|
public let relationshipViewModel = RelationshipViewModel()
|
||||||
|
|
||||||
|
@Published public var userInterfaceStyle: UIUserInterfaceStyle?
|
||||||
|
@Published public var backgroundColor: UIColor?
|
||||||
|
|
||||||
// Author
|
// Author
|
||||||
@Published public var authorBannerImageURL: URL?
|
@Published public var authorBannerImageURL: URL?
|
||||||
|
@ -37,11 +41,35 @@ extension ProfileCardView {
|
||||||
@Published public var isMuting = false
|
@Published public var isMuting = false
|
||||||
@Published public var isBlocking = false
|
@Published public var isBlocking = false
|
||||||
@Published public var isBlockedBy = 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 {
|
extension ProfileCardView.ViewModel {
|
||||||
func bind(view: ProfileCardView) {
|
func bind(view: ProfileCardView) {
|
||||||
|
bindAppearacne(view: view)
|
||||||
bindHeader(view: view)
|
bindHeader(view: view)
|
||||||
bindUser(view: view)
|
bindUser(view: view)
|
||||||
bindBio(view: view)
|
bindBio(view: view)
|
||||||
|
@ -49,6 +77,18 @@ extension ProfileCardView.ViewModel {
|
||||||
bindDashboard(view: view)
|
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) {
|
private func bindHeader(view: ProfileCardView) {
|
||||||
$authorBannerImageURL
|
$authorBannerImageURL
|
||||||
.sink { url in
|
.sink { url in
|
||||||
|
|
|
@ -5,17 +5,23 @@
|
||||||
// Created by MainasuK on 2022-4-14.
|
// Created by MainasuK on 2022-4-14.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import MetaTextKit
|
import MetaTextKit
|
||||||
import MastodonAsset
|
import MastodonAsset
|
||||||
|
|
||||||
|
public protocol ProfileCardViewDelegate: AnyObject {
|
||||||
|
func profileCardView(_ profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
|
||||||
|
}
|
||||||
|
|
||||||
public final class ProfileCardView: UIView {
|
public final class ProfileCardView: UIView {
|
||||||
|
|
||||||
static let avatarSize = CGSize(width: 56, height: 56)
|
static let avatarSize = CGSize(width: 56, height: 56)
|
||||||
static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
|
static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
|
||||||
static let contentMargin: CGFloat = 16
|
static let contentMargin: CGFloat = 16
|
||||||
|
|
||||||
|
weak var delegate: ProfileCardViewDelegate?
|
||||||
private var _disposeBag = Set<AnyCancellable>()
|
private var _disposeBag = Set<AnyCancellable>()
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@ -31,6 +37,7 @@ public final class ProfileCardView: UIView {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// avatar
|
// avatar
|
||||||
|
public let avatarButtonBackgroundView = UIView()
|
||||||
public let avatarButton = AvatarButton()
|
public let avatarButton = AvatarButton()
|
||||||
|
|
||||||
// author name
|
// author name
|
||||||
|
@ -115,7 +122,6 @@ extension ProfileCardView {
|
||||||
statusDashboardView.isUserInteractionEnabled = false
|
statusDashboardView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
// container: V - [ bannerContainer | authorContainer | bioMetaText | infoContainer ]
|
// container: V - [ bannerContainer | authorContainer | bioMetaText | infoContainer ]
|
||||||
container.backgroundColor = Asset.Scene.Discovery.profileCardBackground.color
|
|
||||||
container.axis = .vertical
|
container.axis = .vertical
|
||||||
container.spacing = 8
|
container.spacing = 8
|
||||||
container.translatesAutoresizingMaskIntoConstraints = false
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -171,8 +177,6 @@ extension ProfileCardView {
|
||||||
avatarButton.heightAnchor.constraint(equalToConstant: ProfileCardView.avatarSize.height).priority(.required - 1),
|
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.masksToBounds = true
|
||||||
avatarButtonBackgroundView.layer.cornerCurve = .continuous
|
avatarButtonBackgroundView.layer.cornerCurve = .continuous
|
||||||
avatarButtonBackgroundView.layer.cornerRadius = 12
|
avatarButtonBackgroundView.layer.cornerRadius = 12
|
||||||
|
@ -230,6 +234,22 @@ extension ProfileCardView {
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
bottomPadding.heightAnchor.constraint(equalToConstant: 16)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,8 +8,13 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
public protocol ProfileCardTableViewCellDelegate: AnyObject {
|
||||||
|
func profileCardTableViewCell(_ cell: ProfileCardTableViewCell, profileCardView: ProfileCardView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
|
||||||
|
}
|
||||||
|
|
||||||
public final class ProfileCardTableViewCell: UITableViewCell {
|
public final class ProfileCardTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
public weak var delegate: ProfileCardTableViewCellDelegate?
|
||||||
public var disposeBag = Set<AnyCancellable>()
|
public var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
public let profileCardView: ProfileCardView = {
|
public let profileCardView: ProfileCardView = {
|
||||||
|
@ -63,6 +68,15 @@ extension ProfileCardTableViewCell {
|
||||||
profileCardView.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor),
|
profileCardView.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor),
|
||||||
profileCardView.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor),
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue