Render tags
This commit is contained in:
parent
a7b2c849f9
commit
6b27cd1579
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) } ?? []
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue