Render tags

This commit is contained in:
Justin Mazzocchi 2021-01-23 19:12:30 -08:00
parent a7b2c849f9
commit 6b27cd1579
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
14 changed files with 336 additions and 2 deletions

View file

@ -452,7 +452,8 @@ public extension ContentDatabase {
.init(showContentToggled: $0.showContentToggled,
showAttachmentsToggled: $0.showAttachmentsToggled))
},
titleLocalizedStringKey: "search.statuses")
titleLocalizedStringKey: "search.statuses"),
.init(items: results.hashtags.map(CollectionItem.tag), titleLocalizedStringKey: "search.tags")
]
}
.eraseToAnyPublisher()

View file

@ -8,6 +8,7 @@ public enum CollectionItem: Hashable {
case account(Account)
case notification(MastodonNotification, StatusConfiguration?)
case conversation(Conversation)
case tag(Tag)
}
public extension CollectionItem {
@ -48,6 +49,8 @@ public extension CollectionItem {
return notification.id
case let .conversation(conversation):
return conversation.id
case let .tag(tag):
return tag.name
}
}
}

View file

@ -28,6 +28,8 @@ final class TableViewDataSource: UITableViewDiffableDataSource<CollectionSection
notificationListCell.viewModel = notificationViewModel
case let (conversationListCell as ConversationListCell, conversationViewModel as ConversationViewModel):
conversationListCell.viewModel = conversationViewModel
case let (tagTableViewCell as TagTableViewCell, tagViewModel as TagViewModel):
tagTableViewCell.viewModel = tagViewModel
default:
break
}

View file

@ -9,7 +9,8 @@ extension CollectionItem {
AccountListCell.self,
LoadMoreCell.self,
NotificationListCell.self,
ConversationListCell.self]
ConversationListCell.self,
TagTableViewCell.self]
var cellClass: AnyClass {
switch self {
@ -23,6 +24,8 @@ extension CollectionItem {
return statusConfiguration == nil ? NotificationListCell.self : StatusListCell.self
case .conversation:
return ConversationListCell.self
case .tag:
return TagTableViewCell.self
}
}
@ -49,6 +52,8 @@ extension CollectionItem {
width: width,
identification: identification,
conversation: conversation)
case let .tag(tag):
return TagView.estimatedHeight(width: width, tag: tag)
}
}
}

View file

@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>tag.people-talking</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@people@</string>
<key>people</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>ld</string>
<key>one</key>
<string>%ld person talking</string>
<key>other</key>
<string>%ld people talking</string>
</dict>
</dict>
<key>status.poll.participation-count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View file

@ -5,4 +5,13 @@ import Foundation
public struct Tag: Codable, Hashable {
public let name: String
public let url: URL
public let history: [History]?
}
public extension Tag {
struct History: Codable, Hashable {
public let day: String
public let uses: String
public let accounts: String
}
}

View file

@ -130,6 +130,10 @@
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */; };
D0D2AC3925BBEC0F003D5DF2 /* CollectionSection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */; };
D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4625BCD289003D5DF2 /* TagView.swift */; };
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */; };
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC5225BCD2BA003D5DF2 /* TagContentConfiguration.swift */; };
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */; };
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */; };
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
@ -303,6 +307,10 @@
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
D0CE9F86258B076900E3A6B6 /* AttachmentUploadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadView.swift; sourceTree = "<group>"; };
D0D2AC3825BBEC0F003D5DF2 /* CollectionSection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionSection+Extensions.swift"; sourceTree = "<group>"; };
D0D2AC4625BCD289003D5DF2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagTableViewCell.swift; sourceTree = "<group>"; };
D0D2AC5225BCD2BA003D5DF2 /* TagContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentConfiguration.swift; sourceTree = "<group>"; };
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = "<group>"; };
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
@ -509,6 +517,7 @@
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */,
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */,
D0B8510B25259E56004E0744 /* LoadMoreCell.swift */,
@ -537,6 +546,9 @@
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */,
D0625E55250F086B00502611 /* Status */,
D0C7D42524F76169001EBDBB /* TableView.swift */,
D0D2AC5225BCD2BA003D5DF2 /* TagContentConfiguration.swift */,
D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */,
D0D2AC4625BCD289003D5DF2 /* TagView.swift */,
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */,
D0EA59472522B8B600804347 /* ViewConstants.swift */,
D0F2D54A2581CF7D00986197 /* VisualEffectBlur.swift */,
@ -882,6 +894,8 @@
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */,
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */,
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */,
@ -894,6 +908,7 @@
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */,
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */,
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
D035F8C725B96A4000DC75ED /* SecondaryNavigationButton.swift in Sources */,
@ -918,6 +933,7 @@
D0FCC105259C4E61000B67DF /* NewStatusViewController.swift in Sources */,
D0F2D54B2581CF7D00986197 /* VisualEffectBlur.swift in Sources */,
D087671625BAA8C0001FDD43 /* ExploreViewController.swift in Sources */,
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */,
D0A7AC7325748BFF00E4E8AB /* ReportStatusView.swift in Sources */,
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,

View file

@ -86,6 +86,10 @@ public extension NavigationService {
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
}
func timelineService(timeline: Timeline) -> TimelineService {
TimelineService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
}
}
private extension NavigationService {

View file

@ -154,6 +154,11 @@ extension CollectionItemsViewModel: CollectionViewModel {
.navigation(.collection(collectionService
.navigationService
.contextService(id: status.displayStatus.id))))
case let .tag(tag):
eventsSubject.send(
.navigation(.collection(collectionService
.navigationService
.timelineService(timeline: .tag(tag.name)))))
}
}
@ -255,6 +260,16 @@ extension CollectionItemsViewModel: CollectionViewModel {
cache(viewModel: viewModel, forItem: item)
return viewModel
case let .tag(tag):
if let cachedViewModel = cachedViewModel {
return cachedViewModel
}
let viewModel = TagViewModel(tag: tag)
cache(viewModel: viewModel, forItem: item)
return viewModel
}
}

View file

@ -0,0 +1,50 @@
// Copyright © 2021 Metabolist. All rights reserved.
import Combine
import Foundation
import Mastodon
public struct TagViewModel: CollectionItemViewModel {
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
private let tag: Tag
init(tag: Tag) {
self.tag = tag
events = Empty().eraseToAnyPublisher()
}
}
public extension TagViewModel {
var name: String { "#".appending(tag.name) }
var accounts: Int? {
guard let history = tag.history,
let accountsString = history.first?.accounts,
var accounts = Int(accountsString)
else { return nil }
if history.count > 1, let secondDayAccounts = Int(history[1].accounts) {
accounts += secondDayAccounts
}
return accounts
}
var uses: Int? {
guard let history = tag.history,
let usesString = history.first?.uses,
var uses = Int(usesString)
else { return nil }
if history.count > 1, let secondDayUses = Int(history[1].uses) {
uses += secondDayUses
}
return uses
}
var usageHistory: [Int] {
tag.history?.compactMap { Int($0.uses) } ?? []
}
}

56
Views/LineChartView.swift Normal file
View file

@ -0,0 +1,56 @@
// Copyright © 2021 Metabolist. All rights reserved.
import UIKit
final class LineChartView: UIView {
var values = [Int]() {
didSet { setNeedsDisplay() }
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
UIView.layoutFittingExpandedSize
}
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.lineWidth = Self.lineWidth
path.lineCapStyle = .round
let valueCount = values.count
guard valueCount > 0, let maxValue = values.max() else { return }
for (index, value) in values.enumerated() {
let x = CGFloat(index) / CGFloat(valueCount) * rect.width
let y = rect.height - CGFloat(value) / max(CGFloat(maxValue), CGFloat(0).nextUp) * rect.height
let point = CGPoint(
x: min(max(x, Self.lineWidth / 2), rect.width - Self.lineWidth / 2),
y: min(max(y, Self.lineWidth / 2), rect.height - Self.lineWidth / 2))
if index > 0 {
path.addLine(to: point)
}
path.move(to: point)
}
path.close()
UIColor.link.setStroke()
path.stroke()
}
}
private extension LineChartView {
static let lineWidth: CGFloat = 2
}

View file

@ -0,0 +1,18 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
import ViewModels
struct TagContentConfiguration {
let viewModel: TagViewModel
}
extension TagContentConfiguration: UIContentConfiguration {
func makeContentView() -> UIView & UIContentView {
TagView(configuration: self)
}
func updated(for state: UIConfigurationState) -> TagContentConfiguration {
self
}
}

View file

@ -0,0 +1,26 @@
// Copyright © 2021 Metabolist. All rights reserved.
import UIKit
import ViewModels
final class TagTableViewCell: UITableViewCell {
var viewModel: TagViewModel?
override func updateConfiguration(using state: UICellConfigurationState) {
guard let viewModel = viewModel else { return }
contentConfiguration = TagContentConfiguration(viewModel: viewModel).updated(for: state)
}
override func layoutSubviews() {
super.layoutSubviews()
if UIDevice.current.userInterfaceIdiom == .phone {
separatorInset.left = 0
separatorInset.right = 0
} else {
separatorInset.left = layoutMargins.left
separatorInset.right = layoutMargins.right
}
}
}

113
Views/TagView.swift Normal file
View file

@ -0,0 +1,113 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Mastodon
import UIKit
final class TagView: UIView {
private let nameLabel = UILabel()
private let accountsLabel = UILabel()
private let usesLabel = UILabel()
private let lineChartView = LineChartView()
private var tagConfiguration: TagContentConfiguration
init(configuration: TagContentConfiguration) {
tagConfiguration = configuration
super.init(frame: .zero)
initialSetup()
applyTagConfiguration()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension TagView {
static func estimatedHeight(width: CGFloat, tag: Tag) -> CGFloat {
UITableView.automaticDimension
}
}
extension TagView: UIContentView {
var configuration: UIContentConfiguration {
get { tagConfiguration }
set {
guard let tagConfiguration = newValue as? TagContentConfiguration else { return }
self.tagConfiguration = tagConfiguration
applyTagConfiguration()
}
}
}
private extension TagView {
func initialSetup() {
let stackView = UIStackView()
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = .defaultSpacing
let verticalStackView = UIStackView()
stackView.addArrangedSubview(verticalStackView)
verticalStackView.axis = .vertical
verticalStackView.spacing = .compactSpacing
verticalStackView.addArrangedSubview(nameLabel)
nameLabel.adjustsFontForContentSizeCategory = true
nameLabel.font = .preferredFont(forTextStyle: .headline)
verticalStackView.addArrangedSubview(accountsLabel)
accountsLabel.adjustsFontForContentSizeCategory = true
accountsLabel.font = .preferredFont(forTextStyle: .subheadline)
accountsLabel.textColor = .secondaryLabel
stackView.addArrangedSubview(UIView())
stackView.addArrangedSubview(usesLabel)
usesLabel.adjustsFontForContentSizeCategory = true
usesLabel.font = .preferredFont(forTextStyle: .largeTitle)
usesLabel.setContentHuggingPriority(.required, for: .vertical)
stackView.addArrangedSubview(lineChartView)
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),
lineChartView.heightAnchor.constraint(equalTo: usesLabel.heightAnchor),
lineChartView.widthAnchor.constraint(equalTo: lineChartView.heightAnchor, multiplier: 16 / 9)
])
}
func applyTagConfiguration() {
let viewModel = tagConfiguration.viewModel
nameLabel.text = viewModel.name
if let accounts = viewModel.accounts {
accountsLabel.text = String.localizedStringWithFormat(
NSLocalizedString("tag.people-talking", comment: ""),
accounts)
accountsLabel.isHidden = false
} else {
accountsLabel.isHidden = true
}
if let uses = viewModel.uses {
usesLabel.text = String(uses)
usesLabel.isHidden = false
} else {
usesLabel.isHidden = true
}
lineChartView.values = viewModel.usageHistory.reversed()
lineChartView.isHidden = viewModel.usageHistory.isEmpty
}
}