mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 17:50:59 +00:00
Explore instance section and profile directory
This commit is contained in:
parent
5f03ce7d8b
commit
e62e87510d
14 changed files with 296 additions and 45 deletions
|
@ -8,13 +8,35 @@ import ViewModels
|
||||||
final class ExploreDataSource: UICollectionViewDiffableDataSource<ExploreViewModel.Section, ExploreViewModel.Item> {
|
final class ExploreDataSource: UICollectionViewDiffableDataSource<ExploreViewModel.Section, ExploreViewModel.Item> {
|
||||||
private let updateQueue =
|
private let updateQueue =
|
||||||
DispatchQueue(label: "com.metabolist.metatext.explore-data-source.update-queue")
|
DispatchQueue(label: "com.metabolist.metatext.explore-data-source.update-queue")
|
||||||
|
private weak var collectionView: UICollectionView?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(collectionView: UICollectionView, viewModel: ExploreViewModel) {
|
init(collectionView: UICollectionView, viewModel: ExploreViewModel) {
|
||||||
|
self.collectionView = collectionView
|
||||||
let tagRegistration = UICollectionView.CellRegistration<TagCollectionViewCell, TagViewModel> {
|
let tagRegistration = UICollectionView.CellRegistration<TagCollectionViewCell, TagViewModel> {
|
||||||
$0.viewModel = $2
|
$0.viewModel = $2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let instanceRegistration = UICollectionView.CellRegistration<InstanceCollectionViewCell, InstanceViewModel> {
|
||||||
|
$0.viewModel = $2
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemRegistration = UICollectionView.CellRegistration
|
||||||
|
<SeparatorConfiguredCollectionViewListCell, ExploreViewModel.Item> {
|
||||||
|
var configuration = $0.defaultContentConfiguration()
|
||||||
|
|
||||||
|
switch $2 {
|
||||||
|
case .profileDirectory:
|
||||||
|
configuration.text = NSLocalizedString("explore.profile-directory", comment: "")
|
||||||
|
configuration.image = UIImage(systemName: "person.crop.square.fill.and.at.rectangle")
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.contentConfiguration = configuration
|
||||||
|
$0.accessories = [.disclosureIndicator()]
|
||||||
|
}
|
||||||
|
|
||||||
super.init(collectionView: collectionView) {
|
super.init(collectionView: collectionView) {
|
||||||
switch $2 {
|
switch $2 {
|
||||||
case let .tag(tag):
|
case let .tag(tag):
|
||||||
|
@ -22,6 +44,13 @@ final class ExploreDataSource: UICollectionViewDiffableDataSource<ExploreViewMod
|
||||||
using: tagRegistration,
|
using: tagRegistration,
|
||||||
for: $1,
|
for: $1,
|
||||||
item: viewModel.viewModel(tag: tag))
|
item: viewModel.viewModel(tag: tag))
|
||||||
|
case .instance:
|
||||||
|
return $0.dequeueConfiguredReusableCell(
|
||||||
|
using: instanceRegistration,
|
||||||
|
for: $1,
|
||||||
|
item: viewModel.instanceViewModel)
|
||||||
|
default:
|
||||||
|
return $0.dequeueConfiguredReusableCell(using: itemRegistration, for: $1, item: $2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,19 +63,9 @@ final class ExploreDataSource: UICollectionViewDiffableDataSource<ExploreViewMod
|
||||||
$0.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: $2)
|
$0.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: $2)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.$trends.sink { [weak self] tags in
|
viewModel.$trends.combineLatest(viewModel.$instanceViewModel)
|
||||||
guard let self = self else { return }
|
.sink { [weak self] in self?.update(tags: $0, instanceViewModel: $1) }
|
||||||
|
.store(in: &cancellables)
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<ExploreViewModel.Section, ExploreViewModel.Item>()
|
|
||||||
|
|
||||||
if !tags.isEmpty {
|
|
||||||
snapshot.appendSections([.trending])
|
|
||||||
snapshot.appendItems(tags.map(ExploreViewModel.Item.tag), toSection: .trending)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply(snapshot, animatingDifferences: false)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<ExploreViewModel.Section, ExploreViewModel.Item>,
|
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<ExploreViewModel.Section, ExploreViewModel.Item>,
|
||||||
|
@ -58,6 +77,35 @@ final class ExploreDataSource: UICollectionViewDiffableDataSource<ExploreViewMod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ExploreDataSource {
|
||||||
|
func update(tags: [Tag], instanceViewModel: InstanceViewModel?) {
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<ExploreViewModel.Section, ExploreViewModel.Item>()
|
||||||
|
|
||||||
|
if !tags.isEmpty {
|
||||||
|
snapshot.appendSections([.trending])
|
||||||
|
snapshot.appendItems(tags.map(ExploreViewModel.Item.tag), toSection: .trending)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let instanceViewModel = instanceViewModel {
|
||||||
|
snapshot.appendSections([.instance])
|
||||||
|
snapshot.appendItems([.instance], toSection: .instance)
|
||||||
|
|
||||||
|
if instanceViewModel.instance.canShowProfileDirectory {
|
||||||
|
snapshot.appendItems([.profileDirectory], toSection: .instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let wasEmpty = self.snapshot().itemIdentifiers.isEmpty
|
||||||
|
let contentOffset = collectionView?.contentOffset
|
||||||
|
|
||||||
|
apply(snapshot, animatingDifferences: false) {
|
||||||
|
if let contentOffset = contentOffset, !wasEmpty {
|
||||||
|
self.collectionView?.contentOffset = contentOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension ExploreViewModel.Section {
|
private extension ExploreViewModel.Section {
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
"emoji.system-group.flags" = "Flags";
|
"emoji.system-group.flags" = "Flags";
|
||||||
"explore.trending" = "Trending Now";
|
"explore.trending" = "Trending Now";
|
||||||
"explore.instance" = "Instance";
|
"explore.instance" = "Instance";
|
||||||
|
"explore.profile-directory" = "Profile Directory";
|
||||||
"error" = "Error";
|
"error" = "Error";
|
||||||
"favorites" = "Favorites";
|
"favorites" = "Favorites";
|
||||||
"follow-requests" = "Follow Requests";
|
"follow-requests" = "Follow Requests";
|
||||||
|
|
|
@ -53,3 +53,33 @@ public struct Instance: Codable, Hashable {
|
||||||
self.maxTootChars = maxTootChars
|
self.maxTootChars = maxTootChars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension Instance {
|
||||||
|
var majorVersion: Int? {
|
||||||
|
guard let majorVersionString = version.split(separator: ".").first else { return nil }
|
||||||
|
|
||||||
|
return Int(majorVersionString)
|
||||||
|
}
|
||||||
|
|
||||||
|
var minorVersion: Int? {
|
||||||
|
let versionComponents = version.split(separator: ".")
|
||||||
|
|
||||||
|
guard versionComponents.count > 1 else { return nil }
|
||||||
|
|
||||||
|
return Int(versionComponents[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
var patchVersion: String? {
|
||||||
|
let versionComponents = version.split(separator: ".")
|
||||||
|
|
||||||
|
guard versionComponents.count > 2 else { return nil }
|
||||||
|
|
||||||
|
return String(versionComponents[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
var canShowProfileDirectory: Bool {
|
||||||
|
guard let majorVersion = majorVersion else { return false }
|
||||||
|
|
||||||
|
return majorVersion >= 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ public enum AccountsEndpoint {
|
||||||
case accountsFollowers(id: Account.Id)
|
case accountsFollowers(id: Account.Id)
|
||||||
case accountsFollowing(id: Account.Id)
|
case accountsFollowing(id: Account.Id)
|
||||||
case followRequests
|
case followRequests
|
||||||
|
case directory(local: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountsEndpoint: Endpoint {
|
extension AccountsEndpoint: Endpoint {
|
||||||
|
@ -21,7 +22,7 @@ extension AccountsEndpoint: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .rebloggedBy, .favouritedBy:
|
case .rebloggedBy, .favouritedBy:
|
||||||
return defaultContext + ["statuses"]
|
return defaultContext + ["statuses"]
|
||||||
case .mutes, .blocks, .followRequests:
|
case .mutes, .blocks, .followRequests, .directory:
|
||||||
return defaultContext
|
return defaultContext
|
||||||
case .accountsFollowers, .accountsFollowing:
|
case .accountsFollowers, .accountsFollowing:
|
||||||
return defaultContext + ["accounts"]
|
return defaultContext + ["accounts"]
|
||||||
|
@ -44,6 +45,17 @@ extension AccountsEndpoint: Endpoint {
|
||||||
return [id, "following"]
|
return [id, "following"]
|
||||||
case .followRequests:
|
case .followRequests:
|
||||||
return ["follow_requests"]
|
return ["follow_requests"]
|
||||||
|
case .directory:
|
||||||
|
return ["directory"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var queryParameters: [URLQueryItem] {
|
||||||
|
switch self {
|
||||||
|
case let .directory(local):
|
||||||
|
return [.init(name: "local", value: String(local))]
|
||||||
|
default:
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,10 @@
|
||||||
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; };
|
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EA59472522B8B600804347 /* ViewConstants.swift */; };
|
||||||
D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */; };
|
D097F41B25BE3E1A00859F2C /* SearchScope+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */; };
|
||||||
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */; };
|
D097F4C125BFA04C00859F2C /* NotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */; };
|
||||||
|
D09D970825C64522007E6394 /* InstanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D970725C64522007E6394 /* InstanceView.swift */; };
|
||||||
|
D09D970E25C64539007E6394 /* InstanceContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D970D25C64539007E6394 /* InstanceContentConfiguration.swift */; };
|
||||||
|
D09D971825C64682007E6394 /* InstanceCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D971725C64682007E6394 /* InstanceCollectionViewCell.swift */; };
|
||||||
|
D09D972225C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D972125C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift */; };
|
||||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */; };
|
||||||
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; };
|
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */; };
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B32F4F250B373600311912 /* RegistrationView.swift */; };
|
||||||
|
@ -293,6 +297,10 @@
|
||||||
D08E52ED257D757100FA2C5F /* CompositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionView.swift; sourceTree = "<group>"; };
|
D08E52ED257D757100FA2C5F /* CompositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionView.swift; sourceTree = "<group>"; };
|
||||||
D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchScope+Extensions.swift"; sourceTree = "<group>"; };
|
D097F41A25BE3E1A00859F2C /* SearchScope+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchScope+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = "<group>"; };
|
D097F4C025BFA04C00859F2C /* NotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D09D970725C64522007E6394 /* InstanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceView.swift; sourceTree = "<group>"; };
|
||||||
|
D09D970D25C64539007E6394 /* InstanceContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D09D971725C64682007E6394 /* InstanceCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D09D972125C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorConfiguredCollectionViewListCell.swift; sourceTree = "<group>"; };
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
D0A7AC7225748BFF00E4E8AB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
|
||||||
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = "<group>"; };
|
||||||
|
@ -492,6 +500,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
||||||
|
D09D971725C64682007E6394 /* InstanceCollectionViewCell.swift */,
|
||||||
|
D09D972125C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift */,
|
||||||
D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */,
|
D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = "Collection View Cells";
|
path = "Collection View Cells";
|
||||||
|
@ -505,6 +515,7 @@
|
||||||
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */,
|
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */,
|
||||||
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
|
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
|
||||||
D021A61925C36C1A008A0C0D /* IdentityContentConfiguration.swift */,
|
D021A61925C36C1A008A0C0D /* IdentityContentConfiguration.swift */,
|
||||||
|
D09D970D25C64539007E6394 /* InstanceContentConfiguration.swift */,
|
||||||
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
|
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
|
||||||
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */,
|
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */,
|
||||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */,
|
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */,
|
||||||
|
@ -519,6 +530,7 @@
|
||||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||||
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
||||||
D021A61325C36BFB008A0C0D /* IdentityView.swift */,
|
D021A61325C36BFB008A0C0D /* IdentityView.swift */,
|
||||||
|
D09D970725C64522007E6394 /* InstanceView.swift */,
|
||||||
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
|
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
|
||||||
D036AA06254B6118009094DF /* NotificationView.swift */,
|
D036AA06254B6118009094DF /* NotificationView.swift */,
|
||||||
D00CB2EC2533ACC00080096B /* StatusView.swift */,
|
D00CB2EC2533ACC00080096B /* StatusView.swift */,
|
||||||
|
@ -961,6 +973,7 @@
|
||||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||||
D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */,
|
D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */,
|
||||||
|
D09D970E25C64539007E6394 /* InstanceContentConfiguration.swift in Sources */,
|
||||||
D036AA02254B6101009094DF /* NotificationTableViewCell.swift in Sources */,
|
D036AA02254B6101009094DF /* NotificationTableViewCell.swift in Sources */,
|
||||||
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */,
|
D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */,
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */,
|
||||||
|
@ -989,6 +1002,7 @@
|
||||||
D07EC7F225B13E57006DF726 /* EmojiView.swift in Sources */,
|
D07EC7F225B13E57006DF726 /* EmojiView.swift in Sources */,
|
||||||
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
|
D09D970825C64522007E6394 /* InstanceView.swift in Sources */,
|
||||||
D021A62C25C38570008A0C0D /* AboutView.swift in Sources */,
|
D021A62C25C38570008A0C0D /* AboutView.swift in Sources */,
|
||||||
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
||||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||||
|
@ -1043,9 +1057,11 @@
|
||||||
D0DDA76B25C5F20800FA0F91 /* ExploreDataSource.swift in Sources */,
|
D0DDA76B25C5F20800FA0F91 /* ExploreDataSource.swift in Sources */,
|
||||||
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */,
|
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */,
|
||||||
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
||||||
|
D09D972225C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift in Sources */,
|
||||||
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
||||||
D07EC7FD25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
D07EC7FD25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
||||||
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
||||||
|
D09D971825C64682007E6394 /* InstanceCollectionViewCell.swift in Sources */,
|
||||||
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */,
|
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -35,6 +35,7 @@ final class ExploreViewController: UICollectionViewController {
|
||||||
|
|
||||||
collectionView.dataSource = dataSource
|
collectionView.dataSource = dataSource
|
||||||
collectionView.backgroundColor = .systemBackground
|
collectionView.backgroundColor = .systemBackground
|
||||||
|
collectionView.contentInset.bottom = Self.bottomInset
|
||||||
clearsSelectionOnViewWillAppear = true
|
clearsSelectionOnViewWillAppear = true
|
||||||
|
|
||||||
collectionView.refreshControl = UIRefreshControl()
|
collectionView.refreshControl = UIRefreshControl()
|
||||||
|
@ -92,6 +93,11 @@ final class ExploreViewController: UICollectionViewController {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView,
|
||||||
|
shouldHighlightItemAt indexPath: IndexPath) -> Bool {
|
||||||
|
dataSource.itemIdentifier(for: indexPath) != .instance
|
||||||
|
}
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
@ -115,12 +121,23 @@ extension ExploreViewController: UISearchResultsUpdating {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ExploreViewController {
|
private extension ExploreViewController {
|
||||||
|
static let bottomInset: CGFloat = .newStatusButtonDimension + .defaultSpacing * 4
|
||||||
|
|
||||||
static func layout() -> UICollectionViewLayout {
|
static func layout() -> UICollectionViewLayout {
|
||||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
var listConfiguration = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
|
||||||
config.headerMode = .supplementary
|
listConfiguration.headerMode = .supplementary
|
||||||
|
|
||||||
return UICollectionViewCompositionalLayout.list(using: config)
|
return UICollectionViewCompositionalLayout(
|
||||||
|
sectionProvider: {
|
||||||
|
let section = NSCollectionLayoutSection.list(using: listConfiguration, layoutEnvironment: $1)
|
||||||
|
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
section.contentInsetsReference = .readableContent
|
||||||
|
}
|
||||||
|
|
||||||
|
return section
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(event: ExploreViewModel.Event) {
|
func handle(event: ExploreViewModel.Event) {
|
||||||
|
|
|
@ -49,6 +49,8 @@ public extension ExploreViewModel {
|
||||||
|
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case tag(Tag)
|
case tag(Tag)
|
||||||
|
case instance
|
||||||
|
case profileDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
func refresh() {
|
func refresh() {
|
||||||
|
@ -78,6 +80,14 @@ public extension ExploreViewModel {
|
||||||
.navigation(.collection(exploreService
|
.navigation(.collection(exploreService
|
||||||
.navigationService
|
.navigationService
|
||||||
.timelineService(timeline: .tag(tag.name)))))
|
.timelineService(timeline: .tag(tag.name)))))
|
||||||
|
case .instance:
|
||||||
|
break
|
||||||
|
case .profileDirectory:
|
||||||
|
eventsSubject.send(
|
||||||
|
.navigation(.collection(identityContext
|
||||||
|
.service
|
||||||
|
.service(accountList: .directory(local: true),
|
||||||
|
titleComponents: ["explore.profile-directory"]))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2021 Metabolist. All rights reserved.
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
|
|
||||||
public final class InstanceViewModel: ObservableObject {
|
public final class InstanceViewModel: ObservableObject {
|
||||||
|
@ -10,3 +11,7 @@ public final class InstanceViewModel: ObservableObject {
|
||||||
self.instanceService = instanceService
|
self.instanceService = instanceService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension InstanceViewModel {
|
||||||
|
var instance: Instance { instanceService.instance }
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class InstanceCollectionViewCell: SeparatorConfiguredCollectionViewListCell {
|
||||||
|
var viewModel: InstanceViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = InstanceContentConfiguration(viewModel: viewModel).updated(for: state)
|
||||||
|
updateConstraintsIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SeparatorConfiguredCollectionViewListCell: UICollectionViewListCell {
|
||||||
|
override func updateConstraints() {
|
||||||
|
super.updateConstraints()
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separatorLayoutGuide.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
|
||||||
|
separatorLayoutGuide.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
final class TagCollectionViewCell: UICollectionViewListCell {
|
final class TagCollectionViewCell: SeparatorConfiguredCollectionViewListCell {
|
||||||
var viewModel: TagViewModel?
|
var viewModel: TagViewModel?
|
||||||
|
|
||||||
override func updateConfiguration(using state: UICellConfigurationState) {
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
@ -12,24 +12,4 @@ final class TagCollectionViewCell: UICollectionViewListCell {
|
||||||
contentConfiguration = TagContentConfiguration(viewModel: viewModel).updated(for: state)
|
contentConfiguration = TagContentConfiguration(viewModel: viewModel).updated(for: state)
|
||||||
updateConstraintsIfNeeded()
|
updateConstraintsIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateConstraints() {
|
|
||||||
super.updateConstraints()
|
|
||||||
|
|
||||||
let separatorLeadingAnchor: NSLayoutXAxisAnchor
|
|
||||||
let separatorTrailingAnchor: NSLayoutXAxisAnchor
|
|
||||||
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
|
||||||
separatorLeadingAnchor = readableContentGuide.leadingAnchor
|
|
||||||
separatorTrailingAnchor = readableContentGuide.trailingAnchor
|
|
||||||
} else {
|
|
||||||
separatorLeadingAnchor = leadingAnchor
|
|
||||||
separatorTrailingAnchor = trailingAnchor
|
|
||||||
}
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
separatorLayoutGuide.leadingAnchor.constraint(equalTo: separatorLeadingAnchor),
|
|
||||||
separatorLayoutGuide.trailingAnchor.constraint(equalTo: separatorTrailingAnchor)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct InstanceContentConfiguration {
|
||||||
|
let viewModel: InstanceViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstanceContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
InstanceView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> InstanceContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
84
Views/UIKit/Content Views/InstanceView.swift
Normal file
84
Views/UIKit/Content Views/InstanceView.swift
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class InstanceView: UIView {
|
||||||
|
private let imageView = AnimatedImageView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let uriLabel = UILabel()
|
||||||
|
private var instanceConfiguration: InstanceContentConfiguration
|
||||||
|
|
||||||
|
init(configuration: InstanceContentConfiguration) {
|
||||||
|
instanceConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyInstanceConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InstanceView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { instanceConfiguration }
|
||||||
|
set {
|
||||||
|
guard let instanceConfiguration = newValue as? InstanceContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.instanceConfiguration = instanceConfiguration
|
||||||
|
|
||||||
|
applyInstanceConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension InstanceView {
|
||||||
|
func initialSetup() {
|
||||||
|
let stackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.spacing = .defaultSpacing
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(imageView)
|
||||||
|
imageView.layer.cornerRadius = .defaultCornerRadius
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(titleLabel)
|
||||||
|
titleLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
titleLabel.numberOfLines = 0
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(uriLabel)
|
||||||
|
uriLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
uriLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
uriLabel.numberOfLines = 0
|
||||||
|
uriLabel.textAlignment = .center
|
||||||
|
uriLabel.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
|
stackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
|
||||||
|
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 16 / 9)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyInstanceConfiguration() {
|
||||||
|
let viewModel = instanceConfiguration.viewModel
|
||||||
|
|
||||||
|
imageView.kf.setImage(with: viewModel.instance.thumbnail)
|
||||||
|
|
||||||
|
titleLabel.text = viewModel.instance.title
|
||||||
|
uriLabel.text = viewModel.instance.uri
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,20 +25,21 @@ private extension ExploreSectionHeaderView {
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
label.adjustsFontForContentSizeCategory = true
|
label.adjustsFontForContentSizeCategory = true
|
||||||
label.font = .preferredFont(forTextStyle: .headline)
|
label.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
|
||||||
let layoutGuide: UILayoutGuide
|
let leadingConstraint: NSLayoutConstraint
|
||||||
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
layoutGuide = readableContentGuide
|
leadingConstraint = label.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1)
|
||||||
} else {
|
} else {
|
||||||
layoutGuide = layoutMarginsGuide
|
leadingConstraint = label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
label.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
|
leadingConstraint,
|
||||||
label.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
|
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
label.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
|
label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
label.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor)
|
label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue