Default skin tone selection
This commit is contained in:
parent
dfcc949864
commit
65ba491a4b
|
@ -27,28 +27,7 @@ extension PickerEmoji.Category {
|
|||
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: "")
|
||||
}
|
||||
return NSLocalizedString(group.localizedStringKey, comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import ViewModels
|
||||
|
||||
extension SystemEmoji.SkinTone {
|
||||
static let noneExample = "🖐"
|
||||
|
||||
var example: String {
|
||||
switch self {
|
||||
case .light:
|
||||
return "🖐🏻"
|
||||
case .mediumLight:
|
||||
return "🖐🏼"
|
||||
case .medium:
|
||||
return "🖐🏽"
|
||||
case .mediumDark:
|
||||
return "🖐🏾"
|
||||
case .dark:
|
||||
return "🖐🏿"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SystemEmoji.Group {
|
||||
var localizedStringKey: String {
|
||||
switch self {
|
||||
case .smileysAndEmotion:
|
||||
return "emoji.system-group.smileys-and-emotion"
|
||||
case .peopleAndBody:
|
||||
return "emoji.system-group.people-and-body"
|
||||
case .components:
|
||||
return "emoji.system-group.components"
|
||||
case .animalsAndNature:
|
||||
return "emoji.system-group.animals-and-nature"
|
||||
case .foodAndDrink:
|
||||
return "emoji.system-group.food-and-drink"
|
||||
case .travelAndPlaces:
|
||||
return "emoji.system-group.travel-and-places"
|
||||
case .activites:
|
||||
return "emoji.system-group.activites"
|
||||
case .objects:
|
||||
return "emoji.system-group.objects"
|
||||
case .symbols:
|
||||
return "emoji.system-group.symbols"
|
||||
case .flags:
|
||||
return "emoji.system-group.flags"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,6 +52,7 @@
|
|||
"compose.prompt" = "What's on your mind?";
|
||||
"compose.take-photo-or-video" = "Take Photo or Video";
|
||||
"emoji.custom" = "Custom";
|
||||
"emoji.default-skin-tone" = "Default skin tone";
|
||||
"emoji.frequently-used" = "Frequently used";
|
||||
"emoji.search" = "Search Emoji";
|
||||
"emoji.system-group.smileys-and-emotion" = "Smileys & Emotion";
|
||||
|
|
|
@ -64,6 +64,8 @@
|
|||
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 */; };
|
||||
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */; };
|
||||
D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC81025B232C2006DF726 /* SystemEmoji+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 */; };
|
||||
D088406E25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */; };
|
||||
|
@ -226,6 +228,7 @@
|
|||
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>"; };
|
||||
D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemEmoji+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>"; };
|
||||
D088406C25AFBBE200BB749B /* EmojiPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -573,6 +576,7 @@
|
|||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
|
||||
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */,
|
||||
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
||||
D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */,
|
||||
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */,
|
||||
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
||||
D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
|
||||
|
@ -847,6 +851,7 @@
|
|||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
||||
D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */,
|
||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||
|
@ -922,6 +927,7 @@
|
|||
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
||||
D07EC7E425B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */,
|
||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||
D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
||||
D059370025AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */,
|
||||
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */,
|
||||
|
|
|
@ -2,10 +2,25 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct SystemEmoji: Codable, Hashable {
|
||||
public final class SystemEmoji: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case emoji = "e"
|
||||
case version = "v"
|
||||
case skinToneVariations = "s"
|
||||
case skinTonesPresent = "p"
|
||||
}
|
||||
|
||||
public let emoji: String
|
||||
public let version: Float
|
||||
public let skins: Bool
|
||||
public let skinToneVariations: [SystemEmoji]
|
||||
public let skinTonesPresent: [SkinTone]
|
||||
|
||||
private init(from: SystemEmoji, maxVersionForSkinToneVariations: Float) {
|
||||
emoji = from.emoji
|
||||
version = from.version
|
||||
skinToneVariations = from.skinToneVariations.filter { !($0.version > maxVersionForSkinToneVariations) }
|
||||
skinTonesPresent = from.skinTonesPresent
|
||||
}
|
||||
}
|
||||
|
||||
public extension SystemEmoji {
|
||||
|
@ -21,10 +36,36 @@ public extension SystemEmoji {
|
|||
case symbols
|
||||
case flags
|
||||
}
|
||||
}
|
||||
|
||||
extension SystemEmoji: Comparable {
|
||||
public static func < (lhs: SystemEmoji, rhs: SystemEmoji) -> Bool {
|
||||
lhs.emoji < rhs.emoji
|
||||
enum SkinTone: Int, Codable, Hashable, CaseIterable {
|
||||
case light = 1
|
||||
case mediumLight = 2
|
||||
case medium = 3
|
||||
case mediumDark = 4
|
||||
case dark = 5
|
||||
}
|
||||
|
||||
func withMaxVersionForSkinToneVariations(_ version: Float) -> Self {
|
||||
Self(from: self, maxVersionForSkinToneVariations: version)
|
||||
}
|
||||
|
||||
func applying(skinTone: SkinTone) -> SystemEmoji {
|
||||
skinToneVariations.first { $0.skinTonesPresent.allSatisfy { $0 == skinTone } } ?? self
|
||||
}
|
||||
}
|
||||
|
||||
extension SystemEmoji: Hashable {
|
||||
public static func == (lhs: SystemEmoji, rhs: SystemEmoji) -> Bool {
|
||||
lhs.emoji == rhs.emoji
|
||||
&& lhs.version == rhs.version
|
||||
&& lhs.skinToneVariations == rhs.skinToneVariations
|
||||
&& lhs.skinTonesPresent == rhs.skinTonesPresent
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(emoji)
|
||||
hasher.combine(version)
|
||||
hasher.combine(skinToneVariations)
|
||||
hasher.combine(skinTonesPresent)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -70,8 +70,8 @@ public extension EmojiPickerService {
|
|||
}
|
||||
|
||||
typed[.systemGroup(group)] = emoji
|
||||
.filter { !($0.version > Self.maximumEmojiVersion) }
|
||||
.map(PickerEmoji.system)
|
||||
.filter { !($0.version > Self.maxEmojiVersion) }
|
||||
.map { PickerEmoji.system($0.withMaxVersionForSkinToneVariations(Self.maxEmojiVersion)) }
|
||||
}
|
||||
|
||||
return promise(.success(typed))
|
||||
|
@ -119,7 +119,7 @@ public extension EmojiPickerService {
|
|||
}
|
||||
|
||||
private extension EmojiPickerService {
|
||||
static var maximumEmojiVersion: Float = {
|
||||
static var maxEmojiVersion: Float = {
|
||||
if #available(iOS 14.2, *) {
|
||||
return 13.0
|
||||
}
|
||||
|
|
|
@ -109,6 +109,18 @@ public extension AppPreferences {
|
|||
set { self[.notificationsTabBehavior] = newValue.rawValue }
|
||||
}
|
||||
|
||||
var defaultEmojiSkinTone: SystemEmoji.SkinTone? {
|
||||
get {
|
||||
if let rawValue = self[.defaultEmojiSkinTone] as Int?,
|
||||
let value = SystemEmoji.SkinTone(rawValue: rawValue) {
|
||||
return value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
set { self[.defaultEmojiSkinTone] = newValue?.rawValue }
|
||||
}
|
||||
|
||||
var shouldReduceMotion: Bool {
|
||||
systemReduceMotion() && useSystemReduceMotionForMedia
|
||||
}
|
||||
|
@ -147,6 +159,7 @@ private extension AppPreferences {
|
|||
case autoplayVideos
|
||||
case homeTimelineBehavior
|
||||
case notificationsTabBehavior
|
||||
case defaultEmojiSkinTone
|
||||
}
|
||||
|
||||
subscript<T>(index: Item) -> T? {
|
||||
|
|
|
@ -10,6 +10,7 @@ final class EmojiPickerViewController: UIViewController {
|
|||
private let viewModel: EmojiPickerViewModel
|
||||
private let selectionAction: (PickerEmoji) -> Void
|
||||
private let dismissAction: () -> Void
|
||||
private let skinToneButton = UIButton()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
|
@ -50,8 +51,8 @@ final class EmojiPickerViewController: UIViewController {
|
|||
|
||||
private lazy var dataSource: UICollectionViewDiffableDataSource<PickerEmoji.Category, PickerEmoji> = {
|
||||
let cellRegistration = UICollectionView.CellRegistration
|
||||
<EmojiCollectionViewCell, PickerEmoji> {
|
||||
$0.emoji = $2
|
||||
<EmojiCollectionViewCell, PickerEmoji> { [weak self] in
|
||||
$0.emoji = self?.applyingDefaultSkinTone(emoji: $2) ?? $2
|
||||
}
|
||||
|
||||
let headerRegistration = UICollectionView.SupplementaryRegistration
|
||||
|
@ -71,6 +72,26 @@ final class EmojiPickerViewController: UIViewController {
|
|||
return dataSource
|
||||
}()
|
||||
|
||||
private lazy var defaultSkinToneSelectionMenu: UIMenu = {
|
||||
let clearSkinToneAction = UIAction(title: SystemEmoji.SkinTone.noneExample) { [weak self] _ in
|
||||
self?.skinToneButton.setTitle(SystemEmoji.SkinTone.noneExample, for: .normal)
|
||||
self?.viewModel.identification.appPreferences.defaultEmojiSkinTone = nil
|
||||
self?.reloadVisibleItems()
|
||||
}
|
||||
|
||||
let setSkinToneActions = SystemEmoji.SkinTone.allCases.map { [weak self] skinTone in
|
||||
UIAction(title: skinTone.example) { _ in
|
||||
self?.skinToneButton.setTitle(skinTone.example, for: .normal)
|
||||
self?.viewModel.identification.appPreferences.defaultEmojiSkinTone = skinTone
|
||||
self?.reloadVisibleItems()
|
||||
}
|
||||
}
|
||||
|
||||
return UIMenu(
|
||||
title: NSLocalizedString("emoji.default-skin-tone", comment: ""),
|
||||
children: [clearSkinToneAction] + setSkinToneActions)
|
||||
}()
|
||||
|
||||
init(viewModel: EmojiPickerViewModel,
|
||||
selectionAction: @escaping (PickerEmoji) -> Void,
|
||||
dismissAction: @escaping () -> Void) {
|
||||
|
@ -86,6 +107,7 @@ final class EmojiPickerViewController: UIViewController {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
@ -97,6 +119,15 @@ final class EmojiPickerViewController: UIViewController {
|
|||
UIAction { [weak self] _ in self?.viewModel.query = self?.searchBar.text ?? "" },
|
||||
for: .editingChanged)
|
||||
|
||||
view.addSubview(skinToneButton)
|
||||
skinToneButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
skinToneButton.titleLabel?.adjustsFontSizeToFitWidth = true
|
||||
skinToneButton.setTitle(
|
||||
viewModel.identification.appPreferences.defaultEmojiSkinTone?.example ?? SystemEmoji.SkinTone.noneExample,
|
||||
for: .normal)
|
||||
skinToneButton.showsMenuAsPrimaryAction = true
|
||||
skinToneButton.menu = defaultSkinToneSelectionMenu
|
||||
|
||||
view.addSubview(collectionView)
|
||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
collectionView.backgroundColor = .clear
|
||||
|
@ -106,7 +137,10 @@ final class EmojiPickerViewController: UIViewController {
|
|||
NSLayoutConstraint.activate([
|
||||
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
searchBar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
skinToneButton.leadingAnchor.constraint(equalTo: searchBar.trailingAnchor, constant: .defaultSpacing),
|
||||
skinToneButton.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
skinToneButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
|
||||
skinToneButton.bottomAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
collectionView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
|
@ -114,7 +148,9 @@ final class EmojiPickerViewController: UIViewController {
|
|||
])
|
||||
|
||||
viewModel.$emoji
|
||||
.sink { [weak self] in self?.dataSource.apply($0.snapshot()) }
|
||||
.sink { [weak self] in self?.dataSource.apply(
|
||||
$0.snapshot(),
|
||||
animatingDifferences: !UIAccessibility.isReduceMotionEnabled) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
if let currentKeyboardLanguageIdentifier = searchBar.textInputMode?.primaryLanguage {
|
||||
|
@ -126,11 +162,6 @@ final class EmojiPickerViewController: UIViewController {
|
|||
.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) {
|
||||
|
@ -163,14 +194,52 @@ final class EmojiPickerViewController: UIViewController {
|
|||
|
||||
extension EmojiPickerViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard let emoji = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
selectionAction(emoji)
|
||||
select(emoji: applyingDefaultSkinTone(emoji: item))
|
||||
}
|
||||
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
contextMenuConfigurationForItemAt indexPath: IndexPath,
|
||||
point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||||
case let .system(emoji) = item,
|
||||
!emoji.skinToneVariations.isEmpty
|
||||
else { return nil }
|
||||
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
|
||||
UIMenu(children: ([emoji] + emoji.skinToneVariations).map { skinToneVariation in
|
||||
UIAction(title: skinToneVariation.emoji) { [weak self] _ in
|
||||
self?.select(emoji: .system(skinToneVariation))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension EmojiPickerViewController {
|
||||
static let headerElementKind = "com.metabolist.metatext.emoji-picker.header"
|
||||
|
||||
func select(emoji: PickerEmoji) {
|
||||
selectionAction(emoji)
|
||||
|
||||
UISelectionFeedbackGenerator().selectionChanged()
|
||||
}
|
||||
|
||||
func reloadVisibleItems() {
|
||||
var snapshot = dataSource.snapshot()
|
||||
let visibleItems = collectionView.indexPathsForVisibleItems.compactMap(dataSource.itemIdentifier(for:))
|
||||
|
||||
snapshot.reloadItems(visibleItems)
|
||||
dataSource.apply(snapshot)
|
||||
}
|
||||
|
||||
func applyingDefaultSkinTone(emoji: PickerEmoji) -> PickerEmoji {
|
||||
if case let .system(systemEmoji) = emoji,
|
||||
let defaultEmojiSkinTone = viewModel.identification.appPreferences.defaultEmojiSkinTone {
|
||||
return .system(systemEmoji.applying(skinTone: defaultEmojiSkinTone))
|
||||
} else {
|
||||
return emoji
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ final public class EmojiPickerViewModel: ObservableObject {
|
|||
@Published public var query = ""
|
||||
@Published public var locale = Locale.current
|
||||
@Published public private(set) var emoji = [PickerEmoji.Category: [PickerEmoji]]()
|
||||
public 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]]()
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import ServiceLayer
|
||||
|
||||
public typealias SystemEmoji = ServiceLayer.SystemEmoji
|
Loading…
Reference in New Issue