mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-28 19:11:30 +00:00
Autocomplete wip
This commit is contained in:
parent
2cb8370e68
commit
38ffad5f60
18 changed files with 479 additions and 48 deletions
130
Data Sources/AutocompleteDataSource.swift
Normal file
130
Data Sources/AutocompleteDataSource.swift
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Mastodon
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
enum AutocompleteSection: Int, Hashable {
|
||||||
|
case search
|
||||||
|
case emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AutocompleteItem: Hashable {
|
||||||
|
case account(Account)
|
||||||
|
case tag(Tag)
|
||||||
|
case emoji(PickerEmoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class AutocompleteDataSource: UICollectionViewDiffableDataSource<AutocompleteSection, AutocompleteItem> {
|
||||||
|
@Published private var searchViewModel: SearchViewModel
|
||||||
|
@Published private var emojiPickerViewModel: EmojiPickerViewModel
|
||||||
|
|
||||||
|
private let updateQueue =
|
||||||
|
DispatchQueue(label: "com.metabolist.metatext.autocomplete-data-source.update-queue")
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
init(collectionView: UICollectionView,
|
||||||
|
queryPublisher: AnyPublisher<String?, Never>,
|
||||||
|
parentViewModel: NewStatusViewModel) {
|
||||||
|
searchViewModel = SearchViewModel(identityContext: parentViewModel.identityContext)
|
||||||
|
emojiPickerViewModel = EmojiPickerViewModel(identityContext: parentViewModel.identityContext, queryOnly: true)
|
||||||
|
|
||||||
|
let registration = UICollectionView.CellRegistration<AutocompleteItemCollectionViewCell, AutocompleteItem> {
|
||||||
|
$0.item = $2
|
||||||
|
$0.identityContext = parentViewModel.identityContext
|
||||||
|
}
|
||||||
|
|
||||||
|
let emojiRegistration = UICollectionView.CellRegistration<EmojiCollectionViewCell, PickerEmoji> {
|
||||||
|
$0.emoji = $2.applyingDefaultSkinTone(identityContext: parentViewModel.identityContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init(collectionView: collectionView) {
|
||||||
|
if case let .emoji(emoji) = $2 {
|
||||||
|
return $0.dequeueConfiguredReusableCell(using: emojiRegistration, for: $1, item: emoji)
|
||||||
|
} else {
|
||||||
|
return $0.dequeueConfiguredReusableCell(using: registration, for: $1, item: $2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queryPublisher
|
||||||
|
.replaceNil(with: "")
|
||||||
|
.removeDuplicates()
|
||||||
|
.combineLatest($searchViewModel, $emojiPickerViewModel)
|
||||||
|
.sink(receiveValue: Self.combine(query:searchViewModel:emojiPickerViewModel:))
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
$searchViewModel.map(\.updates)
|
||||||
|
.switchToLatest()
|
||||||
|
.combineLatest($emojiPickerViewModel.map(\.$emoji).switchToLatest())
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] in self?.apply(searchViewModelUpdate: $0, emojiSections: $1) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
parentViewModel.$identityContext
|
||||||
|
.dropFirst()
|
||||||
|
.sink { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.searchViewModel = SearchViewModel(identityContext: $0)
|
||||||
|
self.emojiPickerViewModel = EmojiPickerViewModel(identityContext: $0, queryOnly: true)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func apply(_ snapshot: NSDiffableDataSourceSnapshot<AutocompleteSection, AutocompleteItem>,
|
||||||
|
animatingDifferences: Bool = true,
|
||||||
|
completion: (() -> Void)? = nil) {
|
||||||
|
updateQueue.async {
|
||||||
|
super.apply(snapshot, animatingDifferences: animatingDifferences, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AutocompleteDataSource {
|
||||||
|
static func combine(query: String, searchViewModel: SearchViewModel, emojiPickerViewModel: EmojiPickerViewModel) {
|
||||||
|
if query.starts(with: ":") {
|
||||||
|
searchViewModel.query = ""
|
||||||
|
emojiPickerViewModel.query = String(query.dropFirst())
|
||||||
|
} else {
|
||||||
|
if query.starts(with: "@") {
|
||||||
|
searchViewModel.scope = .accounts
|
||||||
|
} else if query.starts(with: "#") {
|
||||||
|
searchViewModel.scope = .tags
|
||||||
|
}
|
||||||
|
|
||||||
|
searchViewModel.query = String(query.dropFirst())
|
||||||
|
emojiPickerViewModel.query = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(searchViewModelUpdate: CollectionUpdate, emojiSections: [PickerEmoji.Category: [PickerEmoji]]) {
|
||||||
|
var newSnapshot = NSDiffableDataSourceSnapshot<AutocompleteSection, AutocompleteItem>()
|
||||||
|
let items: [AutocompleteItem] = searchViewModelUpdate.sections.map(\.items).reduce([], +).compactMap {
|
||||||
|
switch $0 {
|
||||||
|
case let .account(account, _, _):
|
||||||
|
return .account(account)
|
||||||
|
case let .tag(tag):
|
||||||
|
return .tag(tag)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let emojis = emojiSections.sorted { $0.0 < $1.0 }.map(\.value).reduce([], +).map(AutocompleteItem.emoji)
|
||||||
|
|
||||||
|
newSnapshot.appendSections([.search])
|
||||||
|
|
||||||
|
if !items.isEmpty {
|
||||||
|
newSnapshot.appendItems(items, toSection: .search)
|
||||||
|
} else if !emojis.isEmpty {
|
||||||
|
newSnapshot.appendSections([.emoji])
|
||||||
|
newSnapshot.appendItems(emojis, toSection: .emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(newSnapshot, animatingDifferences: !UIAccessibility.isReduceMotionEnabled) {
|
||||||
|
// animation causes issue with custom emoji images requiring reload
|
||||||
|
newSnapshot.reloadItems(newSnapshot.itemIdentifiers)
|
||||||
|
self.apply(newSnapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,17 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
extension PickerEmoji {
|
||||||
|
func applyingDefaultSkinTone(identityContext: IdentityContext) -> PickerEmoji {
|
||||||
|
if case let .system(systemEmoji, inFrequentlyUsed) = self,
|
||||||
|
let defaultEmojiSkinTone = identityContext.appPreferences.defaultEmojiSkinTone {
|
||||||
|
return .system(systemEmoji.applying(skinTone: defaultEmojiSkinTone), inFrequentlyUsed: inFrequentlyUsed)
|
||||||
|
} else {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension Dictionary where Key == PickerEmoji.Category, Value == [PickerEmoji] {
|
extension Dictionary where Key == PickerEmoji.Category, Value == [PickerEmoji] {
|
||||||
func snapshot() -> NSDiffableDataSourceSnapshot<PickerEmoji.Category, PickerEmoji> {
|
func snapshot() -> NSDiffableDataSourceSnapshot<PickerEmoji.Category, PickerEmoji> {
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<PickerEmoji.Category, PickerEmoji>()
|
var snapshot = NSDiffableDataSourceSnapshot<PickerEmoji.Category, PickerEmoji>()
|
||||||
|
|
|
@ -306,6 +306,7 @@
|
||||||
"tag.accessibility-recent-uses-%ld" = "%ld recent uses";
|
"tag.accessibility-recent-uses-%ld" = "%ld recent uses";
|
||||||
"tag.accessibility-hint.post" = "View posts associated with trend";
|
"tag.accessibility-hint.post" = "View posts associated with trend";
|
||||||
"tag.accessibility-hint.toot" = "View toots associated with trend";
|
"tag.accessibility-hint.toot" = "View toots associated with trend";
|
||||||
|
"tag.per-week-%ld" = "%ld per week";
|
||||||
"timelines.home" = "Home";
|
"timelines.home" = "Home";
|
||||||
"timelines.local" = "Local";
|
"timelines.local" = "Local";
|
||||||
"timelines.federated" = "Federated";
|
"timelines.federated" = "Federated";
|
||||||
|
|
|
@ -165,6 +165,13 @@
|
||||||
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */; };
|
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC4C25BCD2A9003D5DF2 /* TagTableViewCell.swift */; };
|
||||||
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC5225BCD2BA003D5DF2 /* TagContentConfiguration.swift */; };
|
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC5225BCD2BA003D5DF2 /* TagContentConfiguration.swift */; };
|
||||||
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */; };
|
D0D2AC6725BD0484003D5DF2 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */; };
|
||||||
|
D0D93EBA25D9C70400C622ED /* AutocompleteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */; };
|
||||||
|
D0D93EC025D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */; };
|
||||||
|
D0D93EC525D9C75E00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */; };
|
||||||
|
D0D93ECA25D9C76500C622ED /* AutocompleteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */; };
|
||||||
|
D0D93ED025D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93ECF25D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift */; };
|
||||||
|
D0D93ED925D9CBE200C622ED /* AutocompleteItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D93ECF25D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift */; };
|
||||||
|
D0D93EDE25DA014700C622ED /* SeparatorConfiguredCollectionViewListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D972125C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift */; };
|
||||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
|
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD50CA256B1F24004A04F7 /* ReportView.swift */; };
|
||||||
D0DDA76B25C5F20800FA0F91 /* ExploreDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */; };
|
D0DDA76B25C5F20800FA0F91 /* ExploreDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */; };
|
||||||
D0DDA77525C5F73F00FA0F91 /* TagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */; };
|
D0DDA77525C5F73F00FA0F91 /* TagCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */; };
|
||||||
|
@ -173,6 +180,8 @@
|
||||||
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; };
|
||||||
D0E39AB425D8BF88009C10F8 /* UITextInput+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */; };
|
D0E39AB425D8BF88009C10F8 /* UITextInput+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */; };
|
||||||
D0E39ABD25D8C046009C10F8 /* UITextInput+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */; };
|
D0E39ABD25D8C046009C10F8 /* UITextInput+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */; };
|
||||||
|
D0E39B7E25D9AF23009C10F8 /* AutocompleteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39B7D25D9AF23009C10F8 /* AutocompleteDataSource.swift */; };
|
||||||
|
D0E39B8725D9B7FD009C10F8 /* AutocompleteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E39B7D25D9AF23009C10F8 /* AutocompleteDataSource.swift */; };
|
||||||
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; };
|
||||||
D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DA2529319100FA1D72 /* LoadMoreView.swift */; };
|
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E569DA2529319100FA1D72 /* LoadMoreView.swift */; };
|
||||||
|
@ -373,6 +382,9 @@
|
||||||
D0D2AC5225BCD2BA003D5DF2 /* TagContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentConfiguration.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>"; };
|
D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = "<group>"; };
|
||||||
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
|
D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = "<group>"; };
|
||||||
|
D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemView.swift; sourceTree = "<group>"; };
|
||||||
|
D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D0D93ECF25D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteItemCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
|
D0DD50CA256B1F24004A04F7 /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
|
||||||
D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreDataSource.swift; sourceTree = "<group>"; };
|
D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagCollectionViewCell.swift; sourceTree = "<group>"; };
|
D0DDA77425C5F73F00FA0F91 /* TagCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -381,6 +393,7 @@
|
||||||
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
|
D0E1F582251F13EC00D45315 /* WebfingerIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebfingerIndicatorView.swift; sourceTree = "<group>"; };
|
||||||
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
|
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
|
||||||
D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextInput+Extensions.swift"; sourceTree = "<group>"; };
|
D0E39AB325D8BF88009C10F8 /* UITextInput+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextInput+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D0E39B7D25D9AF23009C10F8 /* AutocompleteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteDataSource.swift; sourceTree = "<group>"; };
|
||||||
D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -537,6 +550,7 @@
|
||||||
D021A66F25C3E1F9008A0C0D /* Collection View Cells */ = {
|
D021A66F25C3E1F9008A0C0D /* Collection View Cells */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D0D93ECF25D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift */,
|
||||||
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
||||||
D09D971725C64682007E6394 /* InstanceCollectionViewCell.swift */,
|
D09D971725C64682007E6394 /* InstanceCollectionViewCell.swift */,
|
||||||
D09D972125C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift */,
|
D09D972125C65682007E6394 /* SeparatorConfiguredCollectionViewListCell.swift */,
|
||||||
|
@ -549,6 +563,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */,
|
D0F0B112251A86A000942152 /* AccountContentConfiguration.swift */,
|
||||||
|
D0D93EBF25D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift */,
|
||||||
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
||||||
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */,
|
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */,
|
||||||
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
|
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
|
||||||
|
@ -566,6 +581,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||||
|
D0D93EB925D9C70400C622ED /* AutocompleteItemView.swift */,
|
||||||
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
||||||
D021A61325C36BFB008A0C0D /* IdentityView.swift */,
|
D021A61325C36BFB008A0C0D /* IdentityView.swift */,
|
||||||
D09D970725C64522007E6394 /* InstanceView.swift */,
|
D09D970725C64522007E6394 /* InstanceView.swift */,
|
||||||
|
@ -677,6 +693,7 @@
|
||||||
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
D0A1F4F5252E7D2A004435BF /* Data Sources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D0E39B7D25D9AF23009C10F8 /* AutocompleteDataSource.swift */,
|
||||||
D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */,
|
D0DDA76A25C5F20800FA0F91 /* ExploreDataSource.swift */,
|
||||||
D021A5FF25C3478F008A0C0D /* IdentitiesDataSource.swift */,
|
D021A5FF25C3478F008A0C0D /* IdentitiesDataSource.swift */,
|
||||||
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
D0A1F4F6252E7D4B004435BF /* TableViewDataSource.swift */,
|
||||||
|
@ -1031,6 +1048,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 */,
|
||||||
|
D0D93EC025D9C71D00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */,
|
||||||
D09D970E25C64539007E6394 /* InstanceContentConfiguration.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 */,
|
||||||
|
@ -1069,6 +1087,7 @@
|
||||||
D021A61425C36BFB008A0C0D /* IdentityView.swift in Sources */,
|
D021A61425C36BFB008A0C0D /* IdentityView.swift in Sources */,
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
||||||
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */,
|
D0D2AC4D25BCD2A9003D5DF2 /* TagTableViewCell.swift in Sources */,
|
||||||
|
D0D93ED025D9C9ED00C622ED /* AutocompleteItemCollectionViewCell.swift in Sources */,
|
||||||
D00CB22A25C92C0F008EF267 /* Attachment+Extensions.swift in Sources */,
|
D00CB22A25C92C0F008EF267 /* Attachment+Extensions.swift in Sources */,
|
||||||
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */,
|
D0D2AC5325BCD2BA003D5DF2 /* TagContentConfiguration.swift in Sources */,
|
||||||
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
||||||
|
@ -1077,6 +1096,7 @@
|
||||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||||
D021A60025C3478F008A0C0D /* IdentitiesDataSource.swift in Sources */,
|
D021A60025C3478F008A0C0D /* IdentitiesDataSource.swift in Sources */,
|
||||||
|
D0E39B7E25D9AF23009C10F8 /* AutocompleteDataSource.swift in Sources */,
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
|
||||||
D025B14D25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */,
|
D025B14D25C4E482001C69A8 /* ImageCacheConfiguration.swift in Sources */,
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
|
@ -1107,6 +1127,7 @@
|
||||||
D0BE980425D229D50057E161 /* SeparatorConfiguredTableViewCell.swift in Sources */,
|
D0BE980425D229D50057E161 /* SeparatorConfiguredTableViewCell.swift in Sources */,
|
||||||
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */,
|
D0A1F4F7252E7D4B004435BF /* TableViewDataSource.swift in Sources */,
|
||||||
D025B17E25C500BC001C69A8 /* CapsuleButton.swift in Sources */,
|
D025B17E25C500BC001C69A8 /* CapsuleButton.swift in Sources */,
|
||||||
|
D0D93EBA25D9C70400C622ED /* AutocompleteItemView.swift in Sources */,
|
||||||
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */,
|
||||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */,
|
||||||
D0DDA77F25C6058300FA0F91 /* ExploreSectionHeaderView.swift in Sources */,
|
D0DDA77F25C6058300FA0F91 /* ExploreSectionHeaderView.swift in Sources */,
|
||||||
|
@ -1143,8 +1164,10 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D0D93EDE25DA014700C622ED /* SeparatorConfiguredCollectionViewListCell.swift in Sources */,
|
||||||
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
||||||
D00CB23825C93047008EF267 /* String+Extensions.swift in Sources */,
|
D00CB23825C93047008EF267 /* String+Extensions.swift in Sources */,
|
||||||
|
D0D93EC525D9C75E00C622ED /* AutocompleteItemContentConfiguration.swift in Sources */,
|
||||||
D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
D059373425AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
||||||
D021A67B25C3E32A008A0C0D /* PlayerView.swift in Sources */,
|
D021A67B25C3E32A008A0C0D /* PlayerView.swift in Sources */,
|
||||||
D021A69025C3E4B8008A0C0D /* EmojiContentConfiguration.swift in Sources */,
|
D021A69025C3E4B8008A0C0D /* EmojiContentConfiguration.swift in Sources */,
|
||||||
|
@ -1173,10 +1196,13 @@
|
||||||
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
||||||
D0E39ABD25D8C046009C10F8 /* UITextInput+Extensions.swift in Sources */,
|
D0E39ABD25D8C046009C10F8 /* UITextInput+Extensions.swift in Sources */,
|
||||||
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
||||||
|
D0E39B8725D9B7FD009C10F8 /* AutocompleteDataSource.swift in Sources */,
|
||||||
D00CB23325C92F2D008EF267 /* Attachment+Extensions.swift in Sources */,
|
D00CB23325C92F2D008EF267 /* Attachment+Extensions.swift in Sources */,
|
||||||
|
D0D93ECA25D9C76500C622ED /* AutocompleteItemView.swift in Sources */,
|
||||||
D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */,
|
D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */,
|
||||||
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
||||||
D021A6A625C3E584008A0C0D /* EditAttachmentView.swift in Sources */,
|
D021A6A625C3E584008A0C0D /* EditAttachmentView.swift in Sources */,
|
||||||
|
D0D93ED925D9CBE200C622ED /* AutocompleteItemCollectionViewCell.swift in Sources */,
|
||||||
D0BE97E025D086F80057E161 /* ImagePastableTextView.swift in Sources */,
|
D0BE97E025D086F80057E161 /* ImagePastableTextView.swift in Sources */,
|
||||||
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||||
D021A69525C3E4C1008A0C0D /* EmojiView.swift in Sources */,
|
D021A69525C3E4C1008A0C0D /* EmojiView.swift in Sources */,
|
||||||
|
|
|
@ -20,10 +20,6 @@ public struct ExploreService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ExploreService {
|
public extension ExploreService {
|
||||||
func searchService() -> SearchService {
|
|
||||||
SearchService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
|
||||||
}
|
|
||||||
|
|
||||||
func instanceServicePublisher(uri: String) -> AnyPublisher<InstanceService, Error> {
|
func instanceServicePublisher(uri: String) -> AnyPublisher<InstanceService, Error> {
|
||||||
contentDatabase.instancePublisher(uri: uri)
|
contentDatabase.instancePublisher(uri: uri)
|
||||||
.map { InstanceService(instance: $0, mastodonAPIClient: mastodonAPIClient) }
|
.map { InstanceService(instance: $0, mastodonAPIClient: mastodonAPIClient) }
|
||||||
|
|
|
@ -270,6 +270,10 @@ public extension IdentityService {
|
||||||
ExploreService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
ExploreService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func searchService() -> SearchService {
|
||||||
|
SearchService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
func notificationsService(excludeTypes: Set<MastodonNotification.NotificationType>) -> NotificationsService {
|
func notificationsService(excludeTypes: Set<MastodonNotification.NotificationType>) -> NotificationsService {
|
||||||
NotificationsService(excludeTypes: excludeTypes,
|
NotificationsService(excludeTypes: excludeTypes,
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
|
|
@ -20,7 +20,9 @@ final class EmojiPickerViewController: UICollectionViewController {
|
||||||
private lazy var dataSource: UICollectionViewDiffableDataSource<PickerEmoji.Category, PickerEmoji> = {
|
private lazy var dataSource: UICollectionViewDiffableDataSource<PickerEmoji.Category, PickerEmoji> = {
|
||||||
let cellRegistration = UICollectionView.CellRegistration
|
let cellRegistration = UICollectionView.CellRegistration
|
||||||
<EmojiCollectionViewCell, PickerEmoji> { [weak self] in
|
<EmojiCollectionViewCell, PickerEmoji> { [weak self] in
|
||||||
$0.emoji = self?.applyingDefaultSkinTone(emoji: $2) ?? $2
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
$0.emoji = $2.applyingDefaultSkinTone(identityContext: self.viewModel.identityContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
let headerRegistration = UICollectionView.SupplementaryRegistration
|
let headerRegistration = UICollectionView.SupplementaryRegistration
|
||||||
|
@ -149,9 +151,11 @@ final class EmojiPickerViewController: UICollectionViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
collectionView.deselectItem(at: indexPath, animated: true)
|
||||||
|
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
select(emoji: applyingDefaultSkinTone(emoji: item))
|
select(item.applyingDefaultSkinTone(identityContext: viewModel.identityContext))
|
||||||
viewModel.updateUse(emoji: item)
|
viewModel.updateUse(emoji: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,13 +238,4 @@ private extension EmojiPickerViewController {
|
||||||
snapshot.reloadItems(visibleItems)
|
snapshot.reloadItems(visibleItems)
|
||||||
dataSource.apply(snapshot)
|
dataSource.apply(snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyingDefaultSkinTone(emoji: PickerEmoji) -> PickerEmoji {
|
|
||||||
if case let .system(systemEmoji, inFrequentlyUsed) = emoji,
|
|
||||||
let defaultEmojiSkinTone = viewModel.identityContext.appPreferences.defaultEmojiSkinTone {
|
|
||||||
return .system(systemEmoji.applying(skinTone: defaultEmojiSkinTone), inFrequentlyUsed: inFrequentlyUsed)
|
|
||||||
} else {
|
|
||||||
return emoji
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ final class ProfileViewController: TableViewController {
|
||||||
|
|
||||||
required init(
|
required init(
|
||||||
viewModel: ProfileViewModel,
|
viewModel: ProfileViewModel,
|
||||||
rootViewModel: RootViewModel,
|
rootViewModel: RootViewModel?,
|
||||||
identityContext: IdentityContext,
|
identityContext: IdentityContext,
|
||||||
parentNavigationController: UINavigationController?) {
|
parentNavigationController: UINavigationController?) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
|
@ -13,7 +13,7 @@ class TableViewController: UITableViewController {
|
||||||
var transitionViewTag = -1
|
var transitionViewTag = -1
|
||||||
|
|
||||||
private let viewModel: CollectionViewModel
|
private let viewModel: CollectionViewModel
|
||||||
private let rootViewModel: RootViewModel
|
private let rootViewModel: RootViewModel?
|
||||||
private let loadingTableFooterView = LoadingTableFooterView()
|
private let loadingTableFooterView = LoadingTableFooterView()
|
||||||
private let webfingerIndicatorView = WebfingerIndicatorView()
|
private let webfingerIndicatorView = WebfingerIndicatorView()
|
||||||
@Published private var loading = false
|
@Published private var loading = false
|
||||||
|
@ -29,7 +29,7 @@ class TableViewController: UITableViewController {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
init(viewModel: CollectionViewModel,
|
init(viewModel: CollectionViewModel,
|
||||||
rootViewModel: RootViewModel,
|
rootViewModel: RootViewModel? = nil,
|
||||||
insetBottom: Bool = true,
|
insetBottom: Bool = true,
|
||||||
parentNavigationController: UINavigationController? = nil) {
|
parentNavigationController: UINavigationController? = nil) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
@ -540,7 +540,7 @@ private extension TableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(inReplyToViewModel: StatusViewModel?, redraft: Status?) {
|
func compose(inReplyToViewModel: StatusViewModel?, redraft: Status?) {
|
||||||
rootViewModel.navigationViewModel?.presentedNewStatusViewModel = rootViewModel.newStatusViewModel(
|
rootViewModel?.navigationViewModel?.presentedNewStatusViewModel = rootViewModel?.newStatusViewModel(
|
||||||
identityContext: viewModel.identityContext,
|
identityContext: viewModel.identityContext,
|
||||||
inReplyTo: inReplyToViewModel,
|
inReplyTo: inReplyToViewModel,
|
||||||
redraft: redraft)
|
redraft: redraft)
|
||||||
|
|
|
@ -20,7 +20,7 @@ final public class EmojiPickerViewModel: ObservableObject {
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length
|
||||||
public init(identityContext: IdentityContext) {
|
public init(identityContext: IdentityContext, queryOnly: Bool = false) {
|
||||||
self.identityContext = identityContext
|
self.identityContext = identityContext
|
||||||
emojiPickerService = identityContext.service.emojiPickerService()
|
emojiPickerService = identityContext.service.emojiPickerService()
|
||||||
|
|
||||||
|
@ -67,13 +67,17 @@ final public class EmojiPickerViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if queryOnly {
|
||||||
|
return [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !queryOnly {
|
||||||
emojis[.frequentlyUsed] = emojiUses.compactMap { use in
|
emojis[.frequentlyUsed] = emojiUses.compactMap { use in
|
||||||
emojis.values.reduce([], +)
|
emojis.values.reduce([], +)
|
||||||
.first { use.system == $0.system && use.emoji == $0.name }
|
.first { use.system == $0.system && use.emoji == $0.name }
|
||||||
.map(\.inFrequentlyUsed)
|
.map(\.inFrequentlyUsed)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return emojis.filter { !$0.value.isEmpty }
|
return emojis.filter { !$0.value.isEmpty }
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,7 @@ public final class ExploreViewModel: ObservableObject {
|
||||||
init(service: ExploreService, identityContext: IdentityContext) {
|
init(service: ExploreService, identityContext: IdentityContext) {
|
||||||
exploreService = service
|
exploreService = service
|
||||||
self.identityContext = identityContext
|
self.identityContext = identityContext
|
||||||
searchViewModel = SearchViewModel(
|
searchViewModel = SearchViewModel(identityContext: identityContext)
|
||||||
searchService: exploreService.searchService(),
|
|
||||||
identityContext: identityContext)
|
|
||||||
events = eventsSubject.eraseToAnyPublisher()
|
events = eventsSubject.eraseToAnyPublisher()
|
||||||
|
|
||||||
identityContext.$identity
|
identityContext.$identity
|
||||||
|
|
|
@ -11,8 +11,8 @@ public final class SearchViewModel: CollectionItemsViewModel {
|
||||||
private let searchService: SearchService
|
private let searchService: SearchService
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public init(searchService: SearchService, identityContext: IdentityContext) {
|
public init(identityContext: IdentityContext) {
|
||||||
self.searchService = searchService
|
self.searchService = identityContext.service.searchService()
|
||||||
|
|
||||||
super.init(collectionService: searchService, identityContext: identityContext)
|
super.init(collectionService: searchService, identityContext: identityContext)
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ public final class SearchViewModel: CollectionItemsViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension SearchViewModel {
|
private extension SearchViewModel {
|
||||||
static let debounceInterval: TimeInterval = 0.5
|
static let debounceInterval: TimeInterval = 0.2
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension SearchScope {
|
private extension SearchScope {
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class AutocompleteItemCollectionViewCell: SeparatorConfiguredCollectionViewListCell {
|
||||||
|
var item: AutocompleteItem?
|
||||||
|
var identityContext: IdentityContext?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let item = item, let identityContext = identityContext else { return }
|
||||||
|
|
||||||
|
contentConfiguration = AutocompleteItemContentConfiguration(item: item, identityContext: identityContext)
|
||||||
|
|
||||||
|
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
|
||||||
|
|
||||||
|
backgroundConfiguration.backgroundColor = state.isHighlighted || state.isSelected ? nil : .clear
|
||||||
|
|
||||||
|
self.backgroundConfiguration = backgroundConfiguration
|
||||||
|
|
||||||
|
accessibilityElements = [contentView]
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,5 +10,12 @@ final class EmojiCollectionViewCell: UICollectionViewCell {
|
||||||
guard let emoji = emoji else { return }
|
guard let emoji = emoji else { return }
|
||||||
|
|
||||||
contentConfiguration = EmojiContentConfiguration(emoji: emoji)
|
contentConfiguration = EmojiContentConfiguration(emoji: emoji)
|
||||||
|
|
||||||
|
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
|
||||||
|
|
||||||
|
backgroundConfiguration.backgroundColor = state.isHighlighted || state.isSelected ? nil : .clear
|
||||||
|
backgroundConfiguration.cornerRadius = .defaultCornerRadius
|
||||||
|
|
||||||
|
self.backgroundConfiguration = backgroundConfiguration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,17 @@ import Mastodon
|
||||||
import UIKit
|
import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
final class CompositionInputAccessoryView: UIToolbar {
|
final class CompositionInputAccessoryView: UIView {
|
||||||
let tagForInputView = UUID().hashValue
|
let tagForInputView = UUID().hashValue
|
||||||
|
|
||||||
private let viewModel: CompositionViewModel
|
private let viewModel: CompositionViewModel
|
||||||
private let parentViewModel: NewStatusViewModel
|
private let parentViewModel: NewStatusViewModel
|
||||||
private let autocompleteQueryPublisher: AnyPublisher<String?, Never>
|
private let toolbar = UIToolbar()
|
||||||
|
private let autocompleteCollectionView = UICollectionView(
|
||||||
|
frame: .zero,
|
||||||
|
collectionViewLayout: CompositionInputAccessoryView.autocompleteLayout())
|
||||||
|
private let autocompleteDataSource: AutocompleteDataSource
|
||||||
|
private let autocompleteCollectionViewHeightConstraint: NSLayoutConstraint
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(viewModel: CompositionViewModel,
|
init(viewModel: CompositionViewModel,
|
||||||
|
@ -19,7 +24,12 @@ final class CompositionInputAccessoryView: UIToolbar {
|
||||||
autocompleteQueryPublisher: AnyPublisher<String?, Never>) {
|
autocompleteQueryPublisher: AnyPublisher<String?, Never>) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
self.parentViewModel = parentViewModel
|
self.parentViewModel = parentViewModel
|
||||||
self.autocompleteQueryPublisher = autocompleteQueryPublisher
|
autocompleteDataSource = AutocompleteDataSource(
|
||||||
|
collectionView: autocompleteCollectionView,
|
||||||
|
queryPublisher: autocompleteQueryPublisher,
|
||||||
|
parentViewModel: parentViewModel)
|
||||||
|
autocompleteCollectionViewHeightConstraint =
|
||||||
|
autocompleteCollectionView.heightAnchor.constraint(equalToConstant: .minimumButtonDimension)
|
||||||
|
|
||||||
super.init(
|
super.init(
|
||||||
frame: .init(
|
frame: .init(
|
||||||
|
@ -36,11 +46,42 @@ final class CompositionInputAccessoryView: UIToolbar {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CompositionInputAccessoryView {
|
private extension CompositionInputAccessoryView {
|
||||||
|
static let autocompleteCollectionViewMaxHeight: CGFloat = 150
|
||||||
|
|
||||||
|
var heightConstraint: NSLayoutConstraint? {
|
||||||
|
superview?.constraints.first(where: { $0.identifier == "accessoryHeight" })
|
||||||
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length
|
||||||
func initialSetup() {
|
func initialSetup() {
|
||||||
autoresizingMask = .flexibleHeight
|
autoresizingMask = .flexibleHeight
|
||||||
|
|
||||||
heightAnchor.constraint(equalToConstant: .minimumButtonDimension).isActive = true
|
addSubview(autocompleteCollectionView)
|
||||||
|
autocompleteCollectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
autocompleteCollectionView.alwaysBounceVertical = false
|
||||||
|
autocompleteCollectionView.backgroundColor = .clear
|
||||||
|
autocompleteCollectionView.layer.cornerRadius = .defaultCornerRadius
|
||||||
|
autocompleteCollectionView.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
|
||||||
|
|
||||||
|
let autocompleteBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||||
|
|
||||||
|
autocompleteCollectionView.backgroundView = autocompleteBackgroundView
|
||||||
|
|
||||||
|
addSubview(toolbar)
|
||||||
|
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
toolbar.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
autocompleteCollectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
autocompleteCollectionView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
autocompleteCollectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
autocompleteCollectionView.bottomAnchor.constraint(equalTo: toolbar.topAnchor),
|
||||||
|
toolbar.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
toolbar.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
toolbar.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
toolbar.heightAnchor.constraint(equalToConstant: .minimumButtonDimension),
|
||||||
|
autocompleteCollectionViewHeightConstraint
|
||||||
|
])
|
||||||
|
|
||||||
var attachmentActions = [
|
var attachmentActions = [
|
||||||
UIAction(
|
UIAction(
|
||||||
|
@ -129,15 +170,11 @@ private extension CompositionInputAccessoryView {
|
||||||
NSLocalizedString("compose.add-button-accessibility-label.post", comment: "")
|
NSLocalizedString("compose.add-button-accessibility-label.post", comment: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
let charactersLabel = UILabel()
|
let charactersBarItem = UIBarButtonItem()
|
||||||
|
|
||||||
charactersLabel.font = .preferredFont(forTextStyle: .callout)
|
charactersBarItem.isEnabled = false
|
||||||
charactersLabel.adjustsFontForContentSizeCategory = true
|
|
||||||
charactersLabel.adjustsFontSizeToFitWidth = true
|
|
||||||
|
|
||||||
let charactersBarItem = UIBarButtonItem(customView: charactersLabel)
|
toolbar.items = [
|
||||||
|
|
||||||
items = [
|
|
||||||
attachmentButton,
|
attachmentButton,
|
||||||
UIBarButtonItem.fixedSpace(.defaultSpacing),
|
UIBarButtonItem.fixedSpace(.defaultSpacing),
|
||||||
pollButton,
|
pollButton,
|
||||||
|
@ -162,9 +199,11 @@ private extension CompositionInputAccessoryView {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$remainingCharacters.sink {
|
viewModel.$remainingCharacters.sink {
|
||||||
charactersLabel.text = String($0)
|
charactersBarItem.title = String($0)
|
||||||
charactersLabel.textColor = $0 < 0 ? .systemRed : .label
|
charactersBarItem.setTitleTextAttributes(
|
||||||
charactersLabel.accessibilityLabel = String.localizedStringWithFormat(
|
[.foregroundColor: $0 < 0 ? UIColor.systemRed : UIColor.label],
|
||||||
|
for: .disabled)
|
||||||
|
charactersBarItem.accessibilityHint = String.localizedStringWithFormat(
|
||||||
NSLocalizedString("compose.characters-remaining-accessibility-label-%ld", comment: ""),
|
NSLocalizedString("compose.characters-remaining-accessibility-label-%ld", comment: ""),
|
||||||
$0)
|
$0)
|
||||||
}
|
}
|
||||||
|
@ -174,9 +213,15 @@ private extension CompositionInputAccessoryView {
|
||||||
.sink { addButton.isEnabled = $0 }
|
.sink { addButton.isEnabled = $0 }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
autocompleteQueryPublisher
|
self.autocompleteCollectionView.publisher(for: \.contentSize)
|
||||||
.print()
|
.map(\.height)
|
||||||
.sink { _ in /* TODO */ }
|
.removeDuplicates()
|
||||||
|
.throttle(for: .seconds(TimeInterval.shortAnimationDuration), scheduler: DispatchQueue.main, latest: true)
|
||||||
|
.sink { [weak self] height in
|
||||||
|
UIView.animate(withDuration: .zeroIfReduceMotion(.shortAnimationDuration)) {
|
||||||
|
self?.setAutocompleteCollectionViewHeight(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
parentViewModel.$visibility
|
parentViewModel.$visibility
|
||||||
|
@ -192,6 +237,41 @@ private extension CompositionInputAccessoryView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CompositionInputAccessoryView {
|
private extension CompositionInputAccessoryView {
|
||||||
|
static func autocompleteLayout() -> UICollectionViewLayout {
|
||||||
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
|
|
||||||
|
listConfig.backgroundColor = .clear
|
||||||
|
|
||||||
|
return UICollectionViewCompositionalLayout { index, environment -> NSCollectionLayoutSection? in
|
||||||
|
guard let autocompleteSection = AutocompleteSection(rawValue: index) else { return nil }
|
||||||
|
|
||||||
|
switch autocompleteSection {
|
||||||
|
case .search:
|
||||||
|
return .list(using: listConfig, layoutEnvironment: environment)
|
||||||
|
case .emoji:
|
||||||
|
let itemSize = NSCollectionLayoutSize(
|
||||||
|
widthDimension: .fractionalWidth(1.0),
|
||||||
|
heightDimension: .fractionalHeight(1.0))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
let groupSize = NSCollectionLayoutSize(
|
||||||
|
widthDimension: .absolute(.minimumButtonDimension),
|
||||||
|
heightDimension: .absolute(.minimumButtonDimension))
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
|
||||||
|
section.interGroupSpacing = .defaultSpacing
|
||||||
|
section.orthogonalScrollingBehavior = .continuous
|
||||||
|
section.contentInsets = NSDirectionalEdgeInsets(
|
||||||
|
top: .compactSpacing,
|
||||||
|
leading: .compactSpacing,
|
||||||
|
bottom: .compactSpacing,
|
||||||
|
trailing: .compactSpacing)
|
||||||
|
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func visibilityMenu(selectedVisibility: Status.Visibility) -> UIMenu {
|
func visibilityMenu(selectedVisibility: Status.Visibility) -> UIMenu {
|
||||||
UIMenu(children: Status.Visibility.allCasesExceptUnknown.reversed().map { visibility in
|
UIMenu(children: Status.Visibility.allCasesExceptUnknown.reversed().map { visibility in
|
||||||
UIAction(
|
UIAction(
|
||||||
|
@ -203,4 +283,15 @@ private extension CompositionInputAccessoryView {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setAutocompleteCollectionViewHeight(_ height: CGFloat) {
|
||||||
|
let autocompleteCollectionViewHeight = min(max(height, .hairline), Self.autocompleteCollectionViewMaxHeight)
|
||||||
|
|
||||||
|
autocompleteCollectionViewHeightConstraint.constant = autocompleteCollectionViewHeight
|
||||||
|
autocompleteCollectionView.alpha = autocompleteCollectionViewHeightConstraint.constant == .hairline ? 0 : 1
|
||||||
|
|
||||||
|
heightConstraint?.constant = .minimumButtonDimension + autocompleteCollectionViewHeight
|
||||||
|
updateConstraints()
|
||||||
|
superview?.superview?.layoutIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct AutocompleteItemContentConfiguration {
|
||||||
|
let item: AutocompleteItem
|
||||||
|
let identityContext: IdentityContext
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutocompleteItemContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
AutocompleteItemView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> AutocompleteItemContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import Kingfisher
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
final class EmojiView: UIView {
|
final class EmojiView: UIView {
|
||||||
private let imageView = UIImageView()
|
private let imageView = AnimatedImageView()
|
||||||
private let emojiLabel = UILabel()
|
private let emojiLabel = UILabel()
|
||||||
private var emojiConfiguration: EmojiContentConfiguration
|
private var emojiConfiguration: EmojiContentConfiguration
|
||||||
|
|
||||||
|
|
126
Views/UIKit/Content Views/AutocompleteItemView.swift
Normal file
126
Views/UIKit/Content Views/AutocompleteItemView.swift
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AutocompleteItemView: UIView {
|
||||||
|
private let imageView = AnimatedImageView()
|
||||||
|
private let primaryLabel = UILabel()
|
||||||
|
private let secondaryLabel = UILabel()
|
||||||
|
private let stackView = UIStackView()
|
||||||
|
private var autocompleteItemConfiguration: AutocompleteItemContentConfiguration
|
||||||
|
|
||||||
|
init(configuration: AutocompleteItemContentConfiguration) {
|
||||||
|
self.autocompleteItemConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyAutocompleteItemConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AutocompleteItemView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { autocompleteItemConfiguration }
|
||||||
|
set {
|
||||||
|
guard let autocompleteItemConfiguration = newValue as? AutocompleteItemContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.autocompleteItemConfiguration = autocompleteItemConfiguration
|
||||||
|
|
||||||
|
applyAutocompleteItemConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AutocompleteItemView {
|
||||||
|
func initialSetup() {
|
||||||
|
addSubview(stackView)
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.spacing = .defaultSpacing
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(imageView)
|
||||||
|
imageView.layer.cornerRadius = .barButtonItemDimension / 2
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(primaryLabel)
|
||||||
|
primaryLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
primaryLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
primaryLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
|
||||||
|
stackView.addArrangedSubview(secondaryLabel)
|
||||||
|
secondaryLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
secondaryLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
secondaryLabel.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.widthAnchor.constraint(equalToConstant: .barButtonItemDimension),
|
||||||
|
imageView.heightAnchor.constraint(equalToConstant: .barButtonItemDimension),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
stackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyAutocompleteItemConfiguration() {
|
||||||
|
switch autocompleteItemConfiguration.item {
|
||||||
|
case let .account(account):
|
||||||
|
let appPreferences = autocompleteItemConfiguration.identityContext.appPreferences
|
||||||
|
let avatarURL = appPreferences.animateAvatars == .everywhere
|
||||||
|
&& !appPreferences.shouldReduceMotion
|
||||||
|
? account.avatar
|
||||||
|
: account.avatarStatic
|
||||||
|
|
||||||
|
imageView.kf.setImage(with: avatarURL)
|
||||||
|
imageView.isHidden = false
|
||||||
|
|
||||||
|
let mutableDisplayName = NSMutableAttributedString(string: account.displayName)
|
||||||
|
|
||||||
|
mutableDisplayName.insert(emojis: account.emojis, view: primaryLabel)
|
||||||
|
mutableDisplayName.resizeAttachments(toLineHeight: primaryLabel.font.lineHeight)
|
||||||
|
primaryLabel.attributedText = mutableDisplayName
|
||||||
|
primaryLabel.isHidden = account.displayName.isEmpty
|
||||||
|
secondaryLabel.text = "@".appending(account.acct)
|
||||||
|
primaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
secondaryLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
case let .tag(tag):
|
||||||
|
imageView.isHidden = true
|
||||||
|
imageView.image = nil
|
||||||
|
primaryLabel.text = "#".appending(tag.name)
|
||||||
|
primaryLabel.isHidden = false
|
||||||
|
|
||||||
|
if let uses = tag.history?.compactMap({ Int($0.uses) }).reduce(0, +), uses > 0 {
|
||||||
|
secondaryLabel.text =
|
||||||
|
String.localizedStringWithFormat(NSLocalizedString("tag.per-week-%ld", comment: ""), uses)
|
||||||
|
} else {
|
||||||
|
secondaryLabel.text = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
secondaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let accessibilityAttributedLabel = NSMutableAttributedString(string: "")
|
||||||
|
|
||||||
|
if !primaryLabel.isHidden, let primaryLabelAttributedText = primaryLabel.attributedText {
|
||||||
|
accessibilityAttributedLabel.append(primaryLabelAttributedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let secondaryLabelText = secondaryLabel.text, !secondaryLabelText.isEmpty {
|
||||||
|
accessibilityAttributedLabel.appendWithSeparator(secondaryLabelText)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.accessibilityAttributedLabel = accessibilityAttributedLabel
|
||||||
|
|
||||||
|
isAccessibilityElement = true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue