mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Default skin tone selection
This commit is contained in:
parent
dfcc949864
commit
65ba491a4b
11 changed files with 208 additions and 45 deletions
|
@ -27,28 +27,7 @@ extension PickerEmoji.Category {
|
||||||
case let .customNamed(name):
|
case let .customNamed(name):
|
||||||
return name
|
return name
|
||||||
case let .systemGroup(group):
|
case let .systemGroup(group):
|
||||||
switch group {
|
return NSLocalizedString(group.localizedStringKey, comment: "")
|
||||||
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: "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
Extensions/SystemEmoji+Extensions.swift
Normal file
49
Extensions/SystemEmoji+Extensions.swift
Normal file
|
@ -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.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.custom" = "Custom";
|
||||||
|
"emoji.default-skin-tone" = "Default skin tone";
|
||||||
"emoji.frequently-used" = "Frequently used";
|
"emoji.frequently-used" = "Frequently used";
|
||||||
"emoji.search" = "Search Emoji";
|
"emoji.search" = "Search Emoji";
|
||||||
"emoji.system-group.smileys-and-emotion" = "Smileys & Emotion";
|
"emoji.system-group.smileys-and-emotion" = "Smileys & Emotion";
|
||||||
|
|
|
@ -64,6 +64,8 @@
|
||||||
D07EC7F325B13E57006DF726 /* 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 */; };
|
D07EC7FD25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */; };
|
||||||
D07EC7FE25B16994006DF726 /* 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 */; };
|
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 */; };
|
||||||
|
@ -226,6 +228,7 @@
|
||||||
D07EC7E225B13DD3006DF726 /* EmojiContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiContentConfiguration.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
||||||
|
@ -573,6 +576,7 @@
|
||||||
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
|
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
|
||||||
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */,
|
D0849C7E25903C4900A5EBCC /* Status+Extensions.swift */,
|
||||||
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
||||||
|
D07EC81025B232C2006DF726 /* SystemEmoji+Extensions.swift */,
|
||||||
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */,
|
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */,
|
||||||
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
||||||
D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
|
D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
|
||||||
|
@ -847,6 +851,7 @@
|
||||||
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
D08B8D672540DEB200B1EBEF /* ZoomAnimatableView.swift in Sources */,
|
||||||
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
|
||||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||||
|
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
||||||
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 */,
|
||||||
|
@ -922,6 +927,7 @@
|
||||||
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
D07EC7FE25B16994006DF726 /* EmojiCategoryHeaderView.swift in Sources */,
|
||||||
D07EC7E425B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */,
|
D07EC7E425B13DD3006DF726 /* EmojiContentConfiguration.swift in Sources */,
|
||||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||||
|
D07EC81225B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
|
||||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
||||||
D059370025AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */,
|
D059370025AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift in Sources */,
|
||||||
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */,
|
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */,
|
||||||
|
|
|
@ -2,10 +2,25 @@
|
||||||
|
|
||||||
import Foundation
|
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 emoji: String
|
||||||
public let version: Float
|
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 {
|
public extension SystemEmoji {
|
||||||
|
@ -21,10 +36,36 @@ public extension SystemEmoji {
|
||||||
case symbols
|
case symbols
|
||||||
case flags
|
case flags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SkinTone: Int, Codable, Hashable, CaseIterable {
|
||||||
|
case light = 1
|
||||||
|
case mediumLight = 2
|
||||||
|
case medium = 3
|
||||||
|
case mediumDark = 4
|
||||||
|
case dark = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SystemEmoji: Comparable {
|
func withMaxVersionForSkinToneVariations(_ version: Float) -> Self {
|
||||||
public static func < (lhs: SystemEmoji, rhs: SystemEmoji) -> Bool {
|
Self(from: self, maxVersionForSkinToneVariations: version)
|
||||||
lhs.emoji < rhs.emoji
|
}
|
||||||
|
|
||||||
|
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
|
typed[.systemGroup(group)] = emoji
|
||||||
.filter { !($0.version > Self.maximumEmojiVersion) }
|
.filter { !($0.version > Self.maxEmojiVersion) }
|
||||||
.map(PickerEmoji.system)
|
.map { PickerEmoji.system($0.withMaxVersionForSkinToneVariations(Self.maxEmojiVersion)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise(.success(typed))
|
return promise(.success(typed))
|
||||||
|
@ -119,7 +119,7 @@ public extension EmojiPickerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension EmojiPickerService {
|
private extension EmojiPickerService {
|
||||||
static var maximumEmojiVersion: Float = {
|
static var maxEmojiVersion: Float = {
|
||||||
if #available(iOS 14.2, *) {
|
if #available(iOS 14.2, *) {
|
||||||
return 13.0
|
return 13.0
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,18 @@ public extension AppPreferences {
|
||||||
set { self[.notificationsTabBehavior] = newValue.rawValue }
|
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 {
|
var shouldReduceMotion: Bool {
|
||||||
systemReduceMotion() && useSystemReduceMotionForMedia
|
systemReduceMotion() && useSystemReduceMotionForMedia
|
||||||
}
|
}
|
||||||
|
@ -147,6 +159,7 @@ private extension AppPreferences {
|
||||||
case autoplayVideos
|
case autoplayVideos
|
||||||
case homeTimelineBehavior
|
case homeTimelineBehavior
|
||||||
case notificationsTabBehavior
|
case notificationsTabBehavior
|
||||||
|
case defaultEmojiSkinTone
|
||||||
}
|
}
|
||||||
|
|
||||||
subscript<T>(index: Item) -> T? {
|
subscript<T>(index: Item) -> T? {
|
||||||
|
|
|
@ -10,6 +10,7 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
private let viewModel: EmojiPickerViewModel
|
private let viewModel: EmojiPickerViewModel
|
||||||
private let selectionAction: (PickerEmoji) -> Void
|
private let selectionAction: (PickerEmoji) -> Void
|
||||||
private let dismissAction: () -> Void
|
private let dismissAction: () -> Void
|
||||||
|
private let skinToneButton = UIButton()
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private lazy var collectionView: UICollectionView = {
|
private lazy var collectionView: UICollectionView = {
|
||||||
|
@ -50,8 +51,8 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
|
|
||||||
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> {
|
<EmojiCollectionViewCell, PickerEmoji> { [weak self] in
|
||||||
$0.emoji = $2
|
$0.emoji = self?.applyingDefaultSkinTone(emoji: $2) ?? $2
|
||||||
}
|
}
|
||||||
|
|
||||||
let headerRegistration = UICollectionView.SupplementaryRegistration
|
let headerRegistration = UICollectionView.SupplementaryRegistration
|
||||||
|
@ -71,6 +72,26 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
return dataSource
|
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,
|
init(viewModel: EmojiPickerViewModel,
|
||||||
selectionAction: @escaping (PickerEmoji) -> Void,
|
selectionAction: @escaping (PickerEmoji) -> Void,
|
||||||
dismissAction: @escaping () -> Void) {
|
dismissAction: @escaping () -> Void) {
|
||||||
|
@ -86,6 +107,7 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -97,6 +119,15 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
UIAction { [weak self] _ in self?.viewModel.query = self?.searchBar.text ?? "" },
|
UIAction { [weak self] _ in self?.viewModel.query = self?.searchBar.text ?? "" },
|
||||||
for: .editingChanged)
|
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)
|
view.addSubview(collectionView)
|
||||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
collectionView.backgroundColor = .clear
|
collectionView.backgroundColor = .clear
|
||||||
|
@ -106,7 +137,10 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
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),
|
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.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
collectionView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
collectionView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
|
||||||
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
@ -114,7 +148,9 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
])
|
])
|
||||||
|
|
||||||
viewModel.$emoji
|
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)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
if let currentKeyboardLanguageIdentifier = searchBar.textInputMode?.primaryLanguage {
|
if let currentKeyboardLanguageIdentifier = searchBar.textInputMode?.primaryLanguage {
|
||||||
|
@ -126,11 +162,6 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
.compactMap(Locale.init(identifier:))
|
.compactMap(Locale.init(identifier:))
|
||||||
.assign(to: \.locale, on: viewModel)
|
.assign(to: \.locale, on: viewModel)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
publisher(for: \.isBeingDismissed).print().sink { (_) in
|
|
||||||
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -163,14 +194,52 @@ final class EmojiPickerViewController: UIViewController {
|
||||||
|
|
||||||
extension EmojiPickerViewController: UICollectionViewDelegate {
|
extension EmojiPickerViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
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 {
|
private extension EmojiPickerViewController {
|
||||||
static let headerElementKind = "com.metabolist.metatext.emoji-picker.header"
|
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 query = ""
|
||||||
@Published public var locale = Locale.current
|
@Published public var locale = Locale.current
|
||||||
@Published public private(set) var emoji = [PickerEmoji.Category: [PickerEmoji]]()
|
@Published public private(set) var emoji = [PickerEmoji.Category: [PickerEmoji]]()
|
||||||
|
public let identification: Identification
|
||||||
|
|
||||||
private let identification: Identification
|
|
||||||
private let emojiPickerService: EmojiPickerService
|
private let emojiPickerService: EmojiPickerService
|
||||||
@Published private var customEmoji = [PickerEmoji.Category: [PickerEmoji]]()
|
@Published private var customEmoji = [PickerEmoji.Category: [PickerEmoji]]()
|
||||||
@Published private var systemEmoji = [PickerEmoji.Category: [PickerEmoji]]()
|
@Published private var systemEmoji = [PickerEmoji.Category: [PickerEmoji]]()
|
||||||
|
|
5
ViewModels/Sources/ViewModels/Entities/SystemEmoji.swift
Normal file
5
ViewModels/Sources/ViewModels/Entities/SystemEmoji.swift
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public typealias SystemEmoji = ServiceLayer.SystemEmoji
|
Loading…
Reference in a new issue