From 8deb502140edbe738f86235f323d80dbb92869f6 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 22 Feb 2023 22:23:18 -0500 Subject: [PATCH] Show message on remote profiles with no statuses Closes #279 --- Tusker.xcodeproj/project.pbxproj | 4 + .../ProfileNoContentCollectionViewCell.swift | 81 +++++++++++++++++++ .../ProfileStatusesViewController.swift | 24 +++++- 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 Tusker/Screens/Profile/ProfileNoContentCollectionViewCell.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index a6130518..b6b91aee 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -222,6 +222,7 @@ D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; }; D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */; }; D691771529A6FCAB0054D7EF /* StateRestorableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */; }; + D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; D693A72A25CF8C1E003A14E2 /* ProfileDirectoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */; }; D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; }; @@ -639,6 +640,7 @@ D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = ""; }; D691771029A2B76A0054D7EF /* MainActor+Unsafe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainActor+Unsafe.swift"; sourceTree = ""; }; D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRestorableViewController.swift; sourceTree = ""; }; + D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = ""; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = ""; }; D693A72925CF8C1E003A14E2 /* ProfileDirectoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDirectoryViewController.swift; sourceTree = ""; }; D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = ""; }; @@ -1096,6 +1098,7 @@ D61DC84C28F500D200B82C6E /* ProfileViewController.swift */, D61ABEF728EFC3F900B29151 /* ProfileStatusesViewController.swift */, D61DC84A28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift */, + D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */, ); path = Profile; sourceTree = ""; @@ -1969,6 +1972,7 @@ 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */, D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */, + D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */, D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */, D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */, D60E2F292442372B005F8713 /* AccountMO.swift in Sources */, diff --git a/Tusker/Screens/Profile/ProfileNoContentCollectionViewCell.swift b/Tusker/Screens/Profile/ProfileNoContentCollectionViewCell.swift new file mode 100644 index 00000000..9d9ec3b8 --- /dev/null +++ b/Tusker/Screens/Profile/ProfileNoContentCollectionViewCell.swift @@ -0,0 +1,81 @@ +// +// ProfileNoContentCollectionViewCell.swift +// Tusker +// +// Created by Shadowfacts on 2/22/23. +// Copyright © 2023 Shadowfacts. All rights reserved. +// + +import UIKit + +class ProfileNoContentCollectionViewCell: UICollectionViewListCell { + + weak var delegate: TuskerNavigationDelegate? + private var accountURL: URL? + + private var button: UIButton! + + override init(frame: CGRect) { + super.init(frame: frame) + + let title = UILabel() + title.text = "There's nothing here" + title.adjustsFontForContentSizeCategory = true + title.font = .preferredFont(forTextStyle: .headline) + title.numberOfLines = 0 + title.textAlignment = .center + title.textColor = .secondaryLabel + + let body = UILabel() + body.text = "Your instance may not show all of the posts from accounts on other instances." + body.adjustsFontForContentSizeCategory = true + body.font = .preferredFont(forTextStyle: .body) + body.numberOfLines = 0 + body.textAlignment = .center + body.textColor = .secondaryLabel + + button = UIButton(configuration: .plain(), primaryAction: UIAction(handler: { [unowned self] _ in + if let delegate = self.delegate, + let accountURL = self.accountURL { + delegate.selected(url: accountURL) + } + })) + + let stack = UIStackView(arrangedSubviews: [ + title, + body, + button, + ]) + stack.axis = .vertical + stack.alignment = .center + stack.spacing = 4 + stack.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(stack) + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.leadingAnchor, multiplier: 1), + contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1), + stack.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1), + contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: stack.bottomAnchor, multiplier: 1), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateUI(accountURL: URL) { + self.accountURL = accountURL + + var title: AttributedString = "View on " + var host = AttributedString(accountURL.host!) + host.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold) + title += host + + var config = UIButton.Configuration.plain() + config.attributedTitle = title + config.image = UIImage(systemName: "safari") + config.imagePadding = 4 + button.configuration = config + } + +} diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index c9246221..e474fc44 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -159,6 +159,10 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie } let zeroHeightCell = UICollectionView.CellRegistration { _, _, _ in } + let noContentCell = UICollectionView.CellRegistration { [unowned self] cell, _, item in + cell.delegate = self + cell.updateUI(accountURL: item) + } return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in switch itemIdentifier { case .header(let id): @@ -197,6 +201,8 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie return loadingIndicatorCell(for: indexPath) case .confirmLoadMore: return confirmLoadMoreCell(for: indexPath) + case .noContent(let accountURL): + return collectionView.dequeueConfiguredReusableCell(using: noContentCell, for: indexPath, item: accountURL) } } } @@ -225,7 +231,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie guard isViewLoaded, let accountID, state == .unloaded, - mastodonController.persistentContainer.account(for: accountID) != nil else { + let account = mastodonController.persistentContainer.account(for: accountID) else { return } @@ -246,6 +252,14 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie await controller.loadInitial() await tryLoadPinned() + var newSnapshot = dataSource.snapshot() + if newSnapshot.numberOfItems(inSection: .pinned) == 0, + newSnapshot.numberOfItems(inSection: .statuses) == 0, + account.url.host != mastodonController.instanceURL.host { + newSnapshot.appendItems([.noContent(accountURL: account.url)], toSection: .pinned) + await apply(newSnapshot, animatingDifferences: true) + } + state = .loaded // remove any content inset that was added when switching pages to this VC @@ -410,6 +424,7 @@ extension ProfileStatusesViewController { case status(id: String, collapseState: CollapseState, filterState: FilterState, pinned: Bool) case loadingIndicator case confirmLoadMore + case noContent(accountURL: URL) static func fromTimelineItem(_ item: String) -> Self { return .status(id: item, collapseState: .unknown, filterState: .unknown, pinned: false) @@ -425,6 +440,8 @@ extension ProfileStatusesViewController { return true case (.confirmLoadMore, .confirmLoadMore): return true + case (.noContent(let a), .noContent(let b)): + return a == b default: return false } @@ -443,12 +460,15 @@ extension ProfileStatusesViewController { hasher.combine(2) case .confirmLoadMore: hasher.combine(3) + case .noContent(let accountURL): + hasher.combine(4) + hasher.combine(accountURL) } } var hideSeparators: Bool { switch self { - case .loadingIndicator, .confirmLoadMore: + case .loadingIndicator, .confirmLoadMore, .noContent(_): return true default: return false