mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
wip
This commit is contained in:
parent
def0e3fff0
commit
dfcc949864
42 changed files with 733 additions and 24 deletions
|
@ -132,6 +132,17 @@ extension ContentDatabase {
|
||||||
t.column("wholeWord", .boolean).notNull()
|
t.column("wholeWord", .boolean).notNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try db.create(table: "emoji") { t in
|
||||||
|
t.column("shortcode", .text)
|
||||||
|
.primaryKey(onConflict: .replace)
|
||||||
|
.collate(.localizedCaseInsensitiveCompare)
|
||||||
|
.notNull()
|
||||||
|
t.column("staticUrl", .text).notNull()
|
||||||
|
t.column("url", .text).notNull()
|
||||||
|
t.column("visibleInPicker", .boolean).notNull()
|
||||||
|
t.column("category", .text)
|
||||||
|
}
|
||||||
|
|
||||||
try db.create(table: "conversationRecord") { t in
|
try db.create(table: "conversationRecord") { t in
|
||||||
t.column("id", .text).primaryKey(onConflict: .replace)
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
t.column("unread", .boolean).notNull()
|
t.column("unread", .boolean).notNull()
|
||||||
|
|
|
@ -505,7 +505,10 @@ public extension ContentDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
func pickerEmojisPublisher() -> AnyPublisher<[Emoji], Error> {
|
||||||
ValueObservation.tracking(Emoji.filter(Emoji.Columns.visibleInPicker == true).fetchAll)
|
ValueObservation.tracking(
|
||||||
|
Emoji.filter(Emoji.Columns.visibleInPicker == true)
|
||||||
|
.order(Emoji.Columns.shortcode.asc)
|
||||||
|
.fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseWriter)
|
.publisher(in: databaseWriter)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
|
54
Extensions/PickerEmoji+Extensions.swift
Normal file
54
Extensions/PickerEmoji+Extensions.swift
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
extension Dictionary where Key == PickerEmoji.Category, Value == [PickerEmoji] {
|
||||||
|
func snapshot() -> NSDiffableDataSourceSnapshot<PickerEmoji.Category, PickerEmoji> {
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<PickerEmoji.Category, PickerEmoji>()
|
||||||
|
|
||||||
|
snapshot.appendSections(keys.sorted())
|
||||||
|
|
||||||
|
for (key, value) in self {
|
||||||
|
snapshot.appendItems(value, toSection: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PickerEmoji.Category {
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .frequentlyUsed:
|
||||||
|
return NSLocalizedString("emoji.frequently-used", comment: "")
|
||||||
|
case .custom:
|
||||||
|
return NSLocalizedString("emoji.custom", comment: "")
|
||||||
|
case let .customNamed(name):
|
||||||
|
return name
|
||||||
|
case let .systemGroup(group):
|
||||||
|
switch group {
|
||||||
|
case .smileysAndEmotion:
|
||||||
|
return NSLocalizedString("emoji.system-group.smileys-and-emotion", comment: "")
|
||||||
|
case .peopleAndBody:
|
||||||
|
return NSLocalizedString("emoji.system-group.people-and-body", comment: "")
|
||||||
|
case .components:
|
||||||
|
return NSLocalizedString("emoji.system-group.components", comment: "")
|
||||||
|
case .animalsAndNature:
|
||||||
|
return NSLocalizedString("Animals & Nature", comment: "")
|
||||||
|
case .foodAndDrink:
|
||||||
|
return NSLocalizedString("emoji.system-group.food-and-drink", comment: "")
|
||||||
|
case .travelAndPlaces:
|
||||||
|
return NSLocalizedString("emoji.system-group.travel-and-places", comment: "")
|
||||||
|
case .activites:
|
||||||
|
return NSLocalizedString("emoji.system-group.activites", comment: "")
|
||||||
|
case .objects:
|
||||||
|
return NSLocalizedString("emoji.system-group.objects", comment: "")
|
||||||
|
case .symbols:
|
||||||
|
return NSLocalizedString("emoji.system-group.symbols", comment: "")
|
||||||
|
case .flags:
|
||||||
|
return NSLocalizedString("emoji.system-group.flags", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,19 @@
|
||||||
"compose.poll.allow-multiple-choices" = "Allow multiple choices";
|
"compose.poll.allow-multiple-choices" = "Allow multiple choices";
|
||||||
"compose.prompt" = "What's on your mind?";
|
"compose.prompt" = "What's on your mind?";
|
||||||
"compose.take-photo-or-video" = "Take Photo or Video";
|
"compose.take-photo-or-video" = "Take Photo or Video";
|
||||||
|
"emoji.custom" = "Custom";
|
||||||
|
"emoji.frequently-used" = "Frequently used";
|
||||||
|
"emoji.search" = "Search Emoji";
|
||||||
|
"emoji.system-group.smileys-and-emotion" = "Smileys & Emotion";
|
||||||
|
"emoji.system-group.people-and-body" = "People & Body";
|
||||||
|
"emoji.system-group.components" = "Components";
|
||||||
|
"emoji.system-group.animals-and-nature" = "Animals & Nature";
|
||||||
|
"emoji.system-group.food-and-drink" = "Food & Drink";
|
||||||
|
"emoji.system-group.travel-and-places" = "Travel & Places";
|
||||||
|
"emoji.system-group.activites" = "Activities";
|
||||||
|
"emoji.system-group.objects" = "Objects";
|
||||||
|
"emoji.system-group.symbols" = "Symbols";
|
||||||
|
"emoji.system-group.flags" = "Flags";
|
||||||
"error" = "Error";
|
"error" = "Error";
|
||||||
"favorites" = "Favorites";
|
"favorites" = "Favorites";
|
||||||
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
||||||
|
|
|
@ -54,6 +54,16 @@
|
||||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
||||||
|
D07EC7CF25B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7CE25B13921006DF726 /* PickerEmoji+Extensions.swift */; };
|
||||||
|
D07EC7D025B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7CE25B13921006DF726 /* PickerEmoji+Extensions.swift */; };
|
||||||
|
D07EC7DC25B13DBB006DF726 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */; };
|
||||||
|
D07EC7DD25B13DBB006DF726 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */; };
|
||||||
|
D07EC7E325B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */; };
|
||||||
|
D07EC7E425B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */; };
|
||||||
|
D07EC7F225B13E57006DF726 /* EmojiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7F125B13E57006DF726 /* EmojiView.swift */; };
|
||||||
|
D07EC7F325B13E57006DF726 /* EmojiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7F125B13E57006DF726 /* EmojiView.swift */; };
|
||||||
|
D07EC7FD25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */; };
|
||||||
|
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */; };
|
||||||
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
|
D0849C7F25903C4900A5EBCC /* Status+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */; };
|
||||||
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
||||||
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
||||||
|
@ -211,6 +221,11 @@
|
||||||
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
D06BC5E525202AD90079541D /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D07EC7CE25B13921006DF726 /* PickerEmoji+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PickerEmoji+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D07EC7F125B13E57006DF726 /* EmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiView.swift; sourceTree = "<group>"; };
|
||||||
|
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategoryHeaderView.swift; sourceTree = "<group>"; };
|
||||||
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = "<group>"; };
|
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = "<group>"; };
|
||||||
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewController.swift; sourceTree = "<group>"; };
|
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -473,6 +488,10 @@
|
||||||
D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */,
|
D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */,
|
||||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||||
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
||||||
|
D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */,
|
||||||
|
D07EC7DB25B13DBB006DF726 /* EmojiCollectionViewCell.swift */,
|
||||||
|
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */,
|
||||||
|
D07EC7F125B13E57006DF726 /* EmojiView.swift */,
|
||||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||||
|
@ -550,6 +569,7 @@
|
||||||
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */,
|
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */,
|
||||||
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
|
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
|
||||||
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
|
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
|
||||||
|
D07EC7CE25B13921006DF726 /* PickerEmoji+Extensions.swift */,
|
||||||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
|
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
|
||||||
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */,
|
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */,
|
||||||
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
||||||
|
@ -785,8 +805,10 @@
|
||||||
files = (
|
files = (
|
||||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
||||||
|
D07EC7CF25B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
||||||
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
||||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
||||||
|
D07EC7DC25B13DBB006DF726 /* EmojiCollectionViewCell.swift in Sources */,
|
||||||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
||||||
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
D0B32F50250B373600311912 /* RegistrationView.swift in Sources */,
|
||||||
|
@ -818,6 +840,7 @@
|
||||||
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */,
|
D0E569DB2529319100FA1D72 /* LoadMoreView.swift in Sources */,
|
||||||
D05936FF25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */,
|
D05936FF25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */,
|
||||||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */,
|
||||||
|
D07EC7E325B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */,
|
||||||
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */,
|
||||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */,
|
||||||
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
D08B8D602540DE3B00B1EBEF /* ZoomAnimator.swift in Sources */,
|
||||||
|
@ -827,6 +850,7 @@
|
||||||
D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
||||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||||
|
D07EC7F225B13E57006DF726 /* EmojiView.swift in Sources */,
|
||||||
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
||||||
|
@ -871,6 +895,7 @@
|
||||||
D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */,
|
D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */,
|
||||||
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
|
||||||
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
||||||
|
D07EC7FD25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
||||||
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -894,6 +919,8 @@
|
||||||
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||||
D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */,
|
D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */,
|
||||||
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||||
|
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
||||||
|
D07EC7E425B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */,
|
||||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
||||||
D059370025AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */,
|
D059370025AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */,
|
||||||
|
@ -905,13 +932,16 @@
|
||||||
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
||||||
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
||||||
D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */,
|
D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */,
|
||||||
|
D07EC7DD25B13DBB006DF726 /* EmojiCollectionViewCell.swift in Sources */,
|
||||||
D05936EA25AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
D05936EA25AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
||||||
|
D07EC7D025B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
||||||
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
||||||
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
|
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
|
||||||
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
||||||
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,
|
||||||
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
||||||
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||||
|
D07EC7F325B13E57006DF726 /* EmojiView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,7 +27,8 @@ let package = Package(
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "ServiceLayer",
|
name: "ServiceLayer",
|
||||||
dependencies: ["CodableBloomFilter", "DB", "MastodonAPI", "Secrets"]),
|
dependencies: ["CodableBloomFilter", "DB", "MastodonAPI", "Secrets"],
|
||||||
|
resources: [.process("Resources")]),
|
||||||
.target(
|
.target(
|
||||||
name: "ServiceLayerMocks",
|
name: "ServiceLayerMocks",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|
39
ServiceLayer/Sources/ServiceLayer/Entities/PickerEmoji.swift
Normal file
39
ServiceLayer/Sources/ServiceLayer/Entities/PickerEmoji.swift
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum PickerEmoji: Hashable {
|
||||||
|
case custom(Emoji)
|
||||||
|
case system(SystemEmoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension PickerEmoji {
|
||||||
|
enum Category: Hashable {
|
||||||
|
case frequentlyUsed
|
||||||
|
case custom
|
||||||
|
case customNamed(String)
|
||||||
|
case systemGroup(SystemEmoji.Group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PickerEmoji.Category: Comparable {
|
||||||
|
public static func < (lhs: PickerEmoji.Category, rhs: PickerEmoji.Category) -> Bool {
|
||||||
|
lhs.order < rhs.order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension PickerEmoji.Category {
|
||||||
|
var order: String {
|
||||||
|
switch self {
|
||||||
|
case .frequentlyUsed:
|
||||||
|
return "0"
|
||||||
|
case .custom:
|
||||||
|
return "1"
|
||||||
|
case let .customNamed(name):
|
||||||
|
return "2.\(name)"
|
||||||
|
case let .systemGroup(group):
|
||||||
|
return "3.\(group.rawValue)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
ServiceLayer/Sources/ServiceLayer/Entities/SystemEmoji.swift
Normal file
30
ServiceLayer/Sources/ServiceLayer/Entities/SystemEmoji.swift
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct SystemEmoji: Codable, Hashable {
|
||||||
|
public let emoji: String
|
||||||
|
public let version: Float
|
||||||
|
public let skins: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension SystemEmoji {
|
||||||
|
enum Group: Int, Codable, Hashable, CaseIterable {
|
||||||
|
case smileysAndEmotion
|
||||||
|
case peopleAndBody
|
||||||
|
case components
|
||||||
|
case animalsAndNature
|
||||||
|
case foodAndDrink
|
||||||
|
case travelAndPlaces
|
||||||
|
case activites
|
||||||
|
case objects
|
||||||
|
case symbols
|
||||||
|
case flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SystemEmoji: Comparable {
|
||||||
|
public static func < (lhs: SystemEmoji, rhs: SystemEmoji) -> Bool {
|
||||||
|
lhs.emoji < rhs.emoji
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import DB
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum EmojiPickerError: Error {
|
||||||
|
case invalidLocaleLanguageCode
|
||||||
|
case emojisFileMissing
|
||||||
|
case invalidSystemEmojiGroup
|
||||||
|
case annotationsAndTagsFileMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct EmojiPickerService {
|
||||||
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
|
init(contentDatabase: ContentDatabase) {
|
||||||
|
self.contentDatabase = contentDatabase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension EmojiPickerService {
|
||||||
|
func customEmojiPublisher() -> AnyPublisher<[PickerEmoji.Category: [PickerEmoji]], Error> {
|
||||||
|
contentDatabase.pickerEmojisPublisher().map {
|
||||||
|
var typed = [PickerEmoji.Category: [PickerEmoji]]()
|
||||||
|
|
||||||
|
for emoji in $0 {
|
||||||
|
let category: PickerEmoji.Category
|
||||||
|
|
||||||
|
if let categoryName = emoji.category {
|
||||||
|
category = .customNamed(categoryName)
|
||||||
|
} else {
|
||||||
|
category = .custom
|
||||||
|
}
|
||||||
|
|
||||||
|
if typed[category] == nil {
|
||||||
|
typed[category] = [.custom(emoji)]
|
||||||
|
} else {
|
||||||
|
typed[category]?.append(.custom(emoji))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return typed
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemEmojiPublisher() -> AnyPublisher<[PickerEmoji.Category: [PickerEmoji]], Error> {
|
||||||
|
Future { promise in
|
||||||
|
DispatchQueue.global(qos: .userInteractive).async {
|
||||||
|
guard let url = Bundle.module.url(forResource: "emojis", withExtension: "json") else {
|
||||||
|
promise(.failure(EmojiPickerError.emojisFileMissing))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
let decoded = try JSONDecoder().decode([String: [SystemEmoji]].self, from: data)
|
||||||
|
var typed = [PickerEmoji.Category: [PickerEmoji]]()
|
||||||
|
|
||||||
|
for (groupString, emoji) in decoded {
|
||||||
|
guard let rawValue = Int(groupString),
|
||||||
|
let group = SystemEmoji.Group(rawValue: rawValue)
|
||||||
|
else {
|
||||||
|
promise(.failure(EmojiPickerError.invalidSystemEmojiGroup))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
typed[.systemGroup(group)] = emoji
|
||||||
|
.filter { !($0.version > Self.maximumEmojiVersion) }
|
||||||
|
.map(PickerEmoji.system)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise(.success(typed))
|
||||||
|
} catch {
|
||||||
|
promise(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func systemEmojiAnnotationsAndTagsPublisher(locale: Locale) -> AnyPublisher<[String: String], Error> {
|
||||||
|
Future { promise in
|
||||||
|
guard let languageCode = locale.languageCode else {
|
||||||
|
promise(.failure(EmojiPickerError.invalidLocaleLanguageCode))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let language: String
|
||||||
|
|
||||||
|
if languageCode == "zh" && locale.scriptCode == "Hant" {
|
||||||
|
language = "zh_Hant"
|
||||||
|
} else {
|
||||||
|
language = languageCode
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let url = Bundle.module.url(forResource: language, withExtension: "json") else {
|
||||||
|
promise(.failure(EmojiPickerError.annotationsAndTagsFileMissing))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
let decoded = try JSONDecoder().decode([String: String].self, from: data)
|
||||||
|
|
||||||
|
promise(.success(decoded))
|
||||||
|
} catch {
|
||||||
|
promise(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension EmojiPickerService {
|
||||||
|
static var maximumEmojiVersion: Float = {
|
||||||
|
if #available(iOS 14.2, *) {
|
||||||
|
return 13.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 12.1
|
||||||
|
}()
|
||||||
|
}
|
|
@ -254,6 +254,10 @@ public extension IdentityService {
|
||||||
func domainBlocksService() -> DomainBlocksService {
|
func domainBlocksService() -> DomainBlocksService {
|
||||||
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
DomainBlocksService(mastodonAPIClient: mastodonAPIClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func emojiPickerService() -> EmojiPickerService {
|
||||||
|
EmojiPickerService(contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentityService {
|
private extension IdentityService {
|
||||||
|
|
|
@ -8,10 +8,75 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
let searchBar = UISearchBar()
|
let searchBar = UISearchBar()
|
||||||
|
|
||||||
private let viewModel: EmojiPickerViewModel
|
private let viewModel: EmojiPickerViewModel
|
||||||
|
private let selectionAction: (PickerEmoji) -> Void
|
||||||
|
private let dismissAction: () -> Void
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init(viewModel: EmojiPickerViewModel) {
|
private lazy var collectionView: UICollectionView = {
|
||||||
|
let itemSize = NSCollectionLayoutSize(
|
||||||
|
widthDimension: .absolute(.minimumButtonDimension),
|
||||||
|
heightDimension: .absolute(.minimumButtonDimension))
|
||||||
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
|
let groupSize = NSCollectionLayoutSize(
|
||||||
|
widthDimension: .fractionalWidth(1.0),
|
||||||
|
heightDimension: .estimated(.minimumButtonDimension))
|
||||||
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
|
|
||||||
|
group.interItemSpacing = .flexible(.defaultSpacing)
|
||||||
|
|
||||||
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
|
||||||
|
section.interGroupSpacing = .defaultSpacing
|
||||||
|
section.contentInsets = NSDirectionalEdgeInsets(
|
||||||
|
top: .defaultSpacing,
|
||||||
|
leading: .defaultSpacing,
|
||||||
|
bottom: .defaultSpacing,
|
||||||
|
trailing: .defaultSpacing)
|
||||||
|
|
||||||
|
let headerSize = NSCollectionLayoutSize(
|
||||||
|
widthDimension: .fractionalWidth(1),
|
||||||
|
heightDimension: .estimated(.defaultSpacing))
|
||||||
|
let header = NSCollectionLayoutBoundarySupplementaryItem(
|
||||||
|
layoutSize: headerSize,
|
||||||
|
elementKind: Self.headerElementKind,
|
||||||
|
alignment: .top)
|
||||||
|
|
||||||
|
section.boundarySupplementaryItems = [header]
|
||||||
|
|
||||||
|
let layout = UICollectionViewCompositionalLayout(section: section)
|
||||||
|
|
||||||
|
return UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var dataSource: UICollectionViewDiffableDataSource<PickerEmoji.Category, PickerEmoji> = {
|
||||||
|
let cellRegistration = UICollectionView.CellRegistration
|
||||||
|
<EmojiCollectionViewCell, PickerEmoji> {
|
||||||
|
$0.emoji = $2
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerRegistration = UICollectionView.SupplementaryRegistration
|
||||||
|
<EmojiCategoryHeaderView>(elementKind: "Header") { [weak self] in
|
||||||
|
$0.label.text = self?.dataSource.snapshot().sectionIdentifiers[$2.section].displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataSource = UICollectionViewDiffableDataSource
|
||||||
|
<PickerEmoji.Category, PickerEmoji>(collectionView: collectionView) {
|
||||||
|
$0.dequeueConfiguredReusableCell(using: cellRegistration, for: $1, item: $2)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource.supplementaryViewProvider = {
|
||||||
|
$0.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: $2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(viewModel: EmojiPickerViewModel,
|
||||||
|
selectionAction: @escaping (PickerEmoji) -> Void,
|
||||||
|
dismissAction: @escaping () -> Void) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
self.selectionAction = selectionAction
|
||||||
|
self.dismissAction = dismissAction
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
@ -24,20 +89,48 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
let searchBar = UISearchBar()
|
|
||||||
|
|
||||||
view.addSubview(searchBar)
|
view.addSubview(searchBar)
|
||||||
searchBar.translatesAutoresizingMaskIntoConstraints = false
|
searchBar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
searchBar.searchBarStyle = .minimal
|
searchBar.searchBarStyle = .minimal
|
||||||
|
searchBar.placeholder = NSLocalizedString("emoji.search", comment: "")
|
||||||
|
searchBar.searchTextField.addAction(
|
||||||
|
UIAction { [weak self] _ in self?.viewModel.query = self?.searchBar.text ?? "" },
|
||||||
|
for: .editingChanged)
|
||||||
|
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
collectionView.backgroundColor = .clear
|
||||||
|
collectionView.dataSource = dataSource
|
||||||
|
collectionView.delegate = self
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
searchBar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
searchBar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||||
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
collectionView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||||
|
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
])
|
])
|
||||||
|
|
||||||
// print(UITextInputMode.activeInputModes.map(\.primaryLanguage))
|
viewModel.$emoji
|
||||||
print(Locale.availableIdentifiers)
|
.sink { [weak self] in self?.dataSource.apply($0.snapshot()) }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
if let currentKeyboardLanguageIdentifier = searchBar.textInputMode?.primaryLanguage {
|
||||||
|
viewModel.locale = Locale(identifier: currentKeyboardLanguageIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.publisher(for: UITextInputMode.currentInputModeDidChangeNotification)
|
||||||
|
.compactMap { [weak self] _ in self?.searchBar.textInputMode?.primaryLanguage }
|
||||||
|
.compactMap(Locale.init(identifier:))
|
||||||
|
.assign(to: \.locale, on: viewModel)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
publisher(for: \.isBeingDismissed).print().sink { (_) in
|
||||||
|
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -60,4 +153,24 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
|
|
||||||
setClear(view: containerView)
|
setClear(view: containerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
dismissAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EmojiPickerViewController: UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
guard let emoji = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
|
selectionAction(emoji)
|
||||||
|
|
||||||
|
UISelectionFeedbackGenerator().selectionChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension EmojiPickerViewController {
|
||||||
|
static let headerElementKind = "com.metabolist.metatext.emoji-picker.header"
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,9 +232,10 @@ private extension NewStatusViewController {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
viewModel.$alertItem
|
viewModel.$alertItem
|
||||||
.compactMap { $0 }
|
.compactMap { $0 }
|
||||||
.sink { [weak self] in
|
.sink { [weak self] alertItem in
|
||||||
self?.dismissEmojiPickerIfPresented()
|
self?.dismissEmojiPickerIfPresented {
|
||||||
self?.present(alertItem: $0)
|
self?.present(alertItem: alertItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
@ -271,8 +272,9 @@ private extension NewStatusViewController {
|
||||||
|
|
||||||
picker.modalPresentationStyle = .overFullScreen
|
picker.modalPresentationStyle = .overFullScreen
|
||||||
picker.delegate = self
|
picker.delegate = self
|
||||||
dismissEmojiPickerIfPresented()
|
dismissEmojiPickerIfPresented {
|
||||||
present(picker, animated: true)
|
self.present(picker, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !IS_SHARE_EXTENSION
|
#if !IS_SHARE_EXTENSION
|
||||||
|
@ -323,8 +325,9 @@ private extension NewStatusViewController {
|
||||||
picker.mediaTypes = [UTType.image.description]
|
picker.mediaTypes = [UTType.image.description]
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissEmojiPickerIfPresented()
|
dismissEmojiPickerIfPresented {
|
||||||
present(picker, animated: true)
|
self.present(picker, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -346,8 +349,9 @@ private extension NewStatusViewController {
|
||||||
documentPickerController.delegate = self
|
documentPickerController.delegate = self
|
||||||
documentPickerController.allowsMultipleSelection = false
|
documentPickerController.allowsMultipleSelection = false
|
||||||
documentPickerController.modalPresentationStyle = .overFullScreen
|
documentPickerController.modalPresentationStyle = .overFullScreen
|
||||||
dismissEmojiPickerIfPresented()
|
dismissEmojiPickerIfPresented {
|
||||||
present(documentPickerController, animated: true)
|
self.present(documentPickerController, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentEmojiPicker(tag: Int) {
|
func presentEmojiPicker(tag: Int) {
|
||||||
|
@ -357,11 +361,33 @@ private extension NewStatusViewController {
|
||||||
|
|
||||||
guard let fromView = view.viewWithTag(tag) else { return }
|
guard let fromView = view.viewWithTag(tag) else { return }
|
||||||
|
|
||||||
let emojiPickerController = EmojiPickerViewController(
|
let emojiPickerViewModel = EmojiPickerViewModel(identification: viewModel.identification)
|
||||||
viewModel: .init(identification: viewModel.identification))
|
|
||||||
|
emojiPickerViewModel.$alertItem.assign(to: \.alertItem, on: viewModel).store(in: &cancellables)
|
||||||
|
|
||||||
|
let emojiPickerController = EmojiPickerViewController(viewModel: emojiPickerViewModel) {
|
||||||
|
guard let textInput = fromView as? UITextInput else { return }
|
||||||
|
|
||||||
|
let emojiString: String
|
||||||
|
|
||||||
|
switch $0 {
|
||||||
|
case let .custom(emoji):
|
||||||
|
emojiString = ":\(emoji.shortcode):"
|
||||||
|
case let .system(emoji):
|
||||||
|
emojiString = emoji.emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
if let selectedTextRange = textInput.selectedTextRange {
|
||||||
|
textInput.replace(selectedTextRange, withText: emojiString.appending(" "))
|
||||||
|
}
|
||||||
|
} dismissAction: {
|
||||||
|
fromView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
emojiPickerController.searchBar.inputAccessoryView = fromView.inputAccessoryView
|
emojiPickerController.searchBar.inputAccessoryView = fromView.inputAccessoryView
|
||||||
emojiPickerController.preferredContentSize = view.frame.size
|
emojiPickerController.preferredContentSize = .init(
|
||||||
|
width: view.readableContentGuide.layoutFrame.width,
|
||||||
|
height: view.frame.height)
|
||||||
emojiPickerController.modalPresentationStyle = .popover
|
emojiPickerController.modalPresentationStyle = .popover
|
||||||
emojiPickerController.popoverPresentationController?.delegate = self
|
emojiPickerController.popoverPresentationController?.delegate = self
|
||||||
emojiPickerController.popoverPresentationController?.sourceView = fromView
|
emojiPickerController.popoverPresentationController?.sourceView = fromView
|
||||||
|
@ -372,11 +398,11 @@ private extension NewStatusViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func dismissEmojiPickerIfPresented() -> Bool {
|
func dismissEmojiPickerIfPresented(completion: (() -> Void)? = nil) -> Bool {
|
||||||
let emojiPickerPresented = presentedViewController is EmojiPickerViewController
|
let emojiPickerPresented = presentedViewController is EmojiPickerViewController
|
||||||
|
|
||||||
if emojiPickerPresented {
|
if emojiPickerPresented {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
return emojiPickerPresented
|
return emojiPickerPresented
|
||||||
|
@ -388,8 +414,9 @@ private extension NewStatusViewController {
|
||||||
let navigationController = UINavigationController(rootViewController: editAttachmentViewController)
|
let navigationController = UINavigationController(rootViewController: editAttachmentViewController)
|
||||||
|
|
||||||
navigationController.modalPresentationStyle = .overFullScreen
|
navigationController.modalPresentationStyle = .overFullScreen
|
||||||
dismissEmojiPickerIfPresented()
|
dismissEmojiPickerIfPresented {
|
||||||
present(navigationController, animated: true)
|
self.present(navigationController, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeIdentityButton(identification: Identification) -> UIButton {
|
func changeIdentityButton(identification: Identification) -> UIButton {
|
||||||
|
|
|
@ -2,11 +2,85 @@
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
final public class EmojiPickerViewModel: ObservableObject {
|
final public class EmojiPickerViewModel: ObservableObject {
|
||||||
|
@Published public var alertItem: AlertItem?
|
||||||
|
@Published public var query = ""
|
||||||
|
@Published public var locale = Locale.current
|
||||||
|
@Published public private(set) var emoji = [PickerEmoji.Category: [PickerEmoji]]()
|
||||||
|
|
||||||
private let identification: Identification
|
private let identification: Identification
|
||||||
|
private let emojiPickerService: EmojiPickerService
|
||||||
|
@Published private var customEmoji = [PickerEmoji.Category: [PickerEmoji]]()
|
||||||
|
@Published private var systemEmoji = [PickerEmoji.Category: [PickerEmoji]]()
|
||||||
|
@Published private var systemEmojiAnnotationsAndTags = [String: String]()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public init(identification: Identification) {
|
public init(identification: Identification) {
|
||||||
self.identification = identification
|
self.identification = identification
|
||||||
|
emojiPickerService = identification.service.emojiPickerService()
|
||||||
|
|
||||||
|
emojiPickerService.customEmojiPublisher()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: &$customEmoji)
|
||||||
|
|
||||||
|
emojiPickerService.systemEmojiPublisher()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.assign(to: &$systemEmoji)
|
||||||
|
|
||||||
|
$customEmoji.dropFirst().combineLatest(
|
||||||
|
$systemEmoji.dropFirst(),
|
||||||
|
$query,
|
||||||
|
$locale.combineLatest($systemEmojiAnnotationsAndTags)) // Combine API limits to 4 params
|
||||||
|
.map {
|
||||||
|
let (customEmoji, systemEmoji, query, (locale, systemEmojiAnnotationsAndTags)) = $0
|
||||||
|
|
||||||
|
var queriedCustomEmoji = customEmoji
|
||||||
|
var queriedSystemEmoji = systemEmoji
|
||||||
|
|
||||||
|
if !query.isEmpty {
|
||||||
|
queriedCustomEmoji = queriedCustomEmoji.mapValues {
|
||||||
|
$0.filter {
|
||||||
|
guard case let .custom(emoji) = $0 else { return false }
|
||||||
|
|
||||||
|
return emoji.shortcode.matches(query: query, locale: locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queriedCustomEmoji = queriedCustomEmoji.filter { !$0.value.isEmpty }
|
||||||
|
|
||||||
|
let matchingSystemEmojis = Set(systemEmojiAnnotationsAndTags.filter {
|
||||||
|
$0.key.matches(query: query, locale: locale)
|
||||||
|
}.values)
|
||||||
|
|
||||||
|
queriedSystemEmoji = queriedSystemEmoji.mapValues {
|
||||||
|
$0.filter {
|
||||||
|
guard case let .system(emoji) = $0 else { return false }
|
||||||
|
|
||||||
|
return matchingSystemEmojis.contains(emoji.emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queriedSystemEmoji = queriedSystemEmoji.filter { !$0.value.isEmpty }
|
||||||
|
}
|
||||||
|
|
||||||
|
return queriedSystemEmoji.merging(queriedCustomEmoji) { $1 }
|
||||||
|
}
|
||||||
|
.assign(to: &$emoji)
|
||||||
|
|
||||||
|
$locale.removeDuplicates().flatMap(emojiPickerService.systemEmojiAnnotationsAndTagsPublisher(locale:))
|
||||||
|
.replaceError(with: [:])
|
||||||
|
.assign(to: &$systemEmojiAnnotationsAndTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
func matches(query: String, locale: Locale) -> Bool {
|
||||||
|
lowercased(with: locale)
|
||||||
|
.folding(options: .diacriticInsensitive, locale: locale)
|
||||||
|
.contains(query.lowercased(with: locale)
|
||||||
|
.folding(options: .diacriticInsensitive, locale: locale))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5
ViewModels/Sources/ViewModels/Entities/PickerEmoji.swift
Normal file
5
ViewModels/Sources/ViewModels/Entities/PickerEmoji.swift
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public typealias PickerEmoji = ServiceLayer.PickerEmoji
|
37
Views/EmojiCategoryHeaderView.swift
Normal file
37
Views/EmojiCategoryHeaderView.swift
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class EmojiCategoryHeaderView: UICollectionReusableView {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension EmojiCategoryHeaderView {
|
||||||
|
func initialSetup() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
addSubview(label)
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
label.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
14
Views/EmojiCollectionViewCell.swift
Normal file
14
Views/EmojiCollectionViewCell.swift
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class EmojiCollectionViewCell: UICollectionViewCell {
|
||||||
|
var emoji: PickerEmoji?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let emoji = emoji else { return }
|
||||||
|
|
||||||
|
contentConfiguration = EmojiContentConfiguration(emoji: emoji)
|
||||||
|
}
|
||||||
|
}
|
18
Views/EmojiContentConfiguration.swift
Normal file
18
Views/EmojiContentConfiguration.swift
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct EmojiContentConfiguration {
|
||||||
|
let emoji: PickerEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EmojiContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
EmojiView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> EmojiContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
83
Views/EmojiView.swift
Normal file
83
Views/EmojiView.swift
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright © 2021 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class EmojiView: UIView {
|
||||||
|
private let imageView = UIImageView()
|
||||||
|
private let emojiLabel = UILabel()
|
||||||
|
private var emojiConfiguration: EmojiContentConfiguration
|
||||||
|
|
||||||
|
init(configuration: EmojiContentConfiguration) {
|
||||||
|
emojiConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyEmojiConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EmojiView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { emojiConfiguration }
|
||||||
|
set {
|
||||||
|
guard let emojiConfiguration = newValue as? EmojiContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.emojiConfiguration = emojiConfiguration
|
||||||
|
|
||||||
|
applyEmojiConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension EmojiView {
|
||||||
|
func initialSetup() {
|
||||||
|
layoutMargins = .init(
|
||||||
|
top: .compactSpacing,
|
||||||
|
left: .compactSpacing,
|
||||||
|
bottom: .compactSpacing,
|
||||||
|
right: .compactSpacing)
|
||||||
|
|
||||||
|
addSubview(imageView)
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
addSubview(emojiLabel)
|
||||||
|
emojiLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
emojiLabel.textAlignment = .center
|
||||||
|
emojiLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
emojiLabel.font = .preferredFont(forTextStyle: .largeTitle)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
imageView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
imageView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
imageView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
|
||||||
|
emojiLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
emojiLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
emojiLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
emojiLabel.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyEmojiConfiguration() {
|
||||||
|
switch emojiConfiguration.emoji {
|
||||||
|
case let .custom(emoji):
|
||||||
|
imageView.isHidden = false
|
||||||
|
emojiLabel.isHidden = true
|
||||||
|
|
||||||
|
imageView.kf.setImage(with: emoji.url)
|
||||||
|
case let .system(emoji):
|
||||||
|
imageView.isHidden = true
|
||||||
|
emojiLabel.isHidden = false
|
||||||
|
|
||||||
|
emojiLabel.text = emoji.emoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue