refactor: persist recent used emojis

pull/638/head
Ryan Zhao 2 years ago
parent 220a9ac4a1
commit c91bdb3aeb

@ -137,6 +137,7 @@
7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */; };
7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */; };
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */; };
7B1B52E2286030DF006069F2 /* Storage+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52E1286030DF006069F2 /* Storage+Emoji.swift */; };
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; };
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; };
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
@ -1144,6 +1145,7 @@
7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerSheet.swift; sourceTree = "<group>"; };
7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerCollectionView.swift; sourceTree = "<group>"; };
7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiSkinTonePicker.swift; sourceTree = "<group>"; };
7B1B52E1286030DF006069F2 /* Storage+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Emoji.swift"; sourceTree = "<group>"; };
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = "<group>"; };
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
@ -2160,6 +2162,7 @@
7B9F71CD2852EEE2006DFE7B /* Emoji+SkinTones.swift */,
7B9F71CE2852EEE2006DFE7B /* Emoji.swift */,
7B9F71CF2852EEE2006DFE7B /* Emoji+Name.swift */,
7B1B52E1286030DF006069F2 /* Storage+Emoji.swift */,
);
path = Emoji;
sourceTree = "<group>";
@ -5092,6 +5095,7 @@
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */,
C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */,
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,
7B1B52E2286030DF006069F2 /* Storage+Emoji.swift in Sources */,
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */,
7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */,
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */,

@ -95,7 +95,7 @@ protocol ContextMenuActionDelegate : AnyObject {
func save(_ viewItem: ConversationViewItem)
func ban(_ viewItem: ConversationViewItem)
func banAndDeleteAllMessages(_ viewItem: ConversationViewItem)
func react(_ viewItem: ConversationViewItem, with emoji: String)
func react(_ viewItem: ConversationViewItem, with emoji: EmojiWithSkinTones)
func showFullEmojiKeyboard(_ viewItem: ConversationViewItem)
func contextMenuDismissed()
}

@ -2,7 +2,7 @@
extension ContextMenuVC {
final class EmojiReactsView: UIView {
private let emoji: String
private let emoji: EmojiWithSkinTones
private let dismiss: () -> Void
private let work: () -> Void
@ -10,7 +10,7 @@ extension ContextMenuVC {
private static let size: CGFloat = 40
// MARK: Lifecycle
init(for emoji: String, dismiss: @escaping () -> Void, work: @escaping () -> Void) {
init(for emoji: EmojiWithSkinTones, dismiss: @escaping () -> Void, work: @escaping () -> Void) {
self.emoji = emoji
self.dismiss = dismiss
self.work = work
@ -28,7 +28,7 @@ extension ContextMenuVC {
private func setUpViewHierarchy() {
let emojiLabel = UILabel()
emojiLabel.text = self.emoji
emojiLabel.text = self.emoji.rawValue
emojiLabel.font = .systemFont(ofSize: Values.veryLargeFontSize)
emojiLabel.set(.height, to: ContextMenuVC.EmojiReactsView.size)
addSubview(emojiLabel)

@ -5,6 +5,8 @@ final class ContextMenuVC : UIViewController {
private let frame: CGRect
private let dismiss: () -> Void
private weak var delegate: ContextMenuActionDelegate?
private var recentEmoji: [EmojiWithSkinTones] = []
// MARK: UI Components
private lazy var blurView = UIVisualEffectView(effect: nil)
@ -58,6 +60,9 @@ final class ContextMenuVC : UIViewController {
self.delegate = delegate
self.dismiss = dismiss
super.init(nibName: nil, bundle: nil)
Storage.read { transaction in
self.recentEmoji = Array(Storage.shared.getRecentEmoji(transaction: transaction)[...5])
}
}
override init(nibName: String?, bundle: Bundle?) {
@ -106,7 +111,7 @@ final class ContextMenuVC : UIViewController {
emojiPlusButton.pin(.right, to: .right, of: emojiBar, withInset: -Values.smallSpacing)
emojiPlusButton.center(.vertical, in: emojiBar)
let emojiLabels = UserDefaults.standard.getRecentlyUsedEmojis().map { emoji -> EmojiReactsView in
let emojiLabels = recentEmoji.map { emoji -> EmojiReactsView in
EmojiReactsView(for: emoji, dismiss: snDismiss) {
self.delegate?.react(self.viewItem, with: emoji)
}

@ -822,7 +822,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
presentAlert(alert)
}
func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: String?) {
func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: EmojiWithSkinTones?) {
guard let thread = thread as? TSGroupThread else { return }
guard let message = viewItem.interaction as? TSMessage, message.reactions.count > 0 else { return }
let reactionListSheet = ReactionListSheet(for: viewItem, thread: thread)
@ -833,17 +833,19 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
present(reactionListSheet, animated: true, completion: nil)
}
func react(_ viewItem: ConversationViewItem, with emoji: String) {
UserDefaults.standard.addNewRecentlyUsedEmoji(emoji)
react(viewItem, with: emoji, cancel: false)
func react(_ viewItem: ConversationViewItem, with emoji: EmojiWithSkinTones) {
Storage.write { transaction in
Storage.shared.recordRecentEmoji(emoji, transaction: transaction)
}
react(viewItem, with: emoji.rawValue, cancel: false)
}
func quickReact(_ viewItem: ConversationViewItem, with emoji: String) {
func quickReact(_ viewItem: ConversationViewItem, with emoji: EmojiWithSkinTones) {
react(viewItem, with: emoji)
}
func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) {
react(viewItem, with: emoji, cancel: true)
func cancelReact(_ viewItem: ConversationViewItem, for emoji: EmojiWithSkinTones) {
react(viewItem, with: emoji.rawValue, cancel: true)
}
func cancelAllReact(reactMessages: [ReactMessage]) {
@ -885,7 +887,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
let emojiPicker = EmojiPickerSheet(
completionHandler: { emoji in
if let emoji = emoji {
self.react(viewItem, with: emoji.rawValue)
self.react(viewItem, with: emoji)
}
},
dismissHandler: {

@ -7,9 +7,6 @@ protocol EmojiPickerCollectionViewDelegate: AnyObject {
class EmojiPickerCollectionView: UICollectionView {
let layout: UICollectionViewFlowLayout
private static let emojiPickerCollection = "EmojiPickerCollection"
private static let recentEmojiKey = "recentEmoji"
weak var pickerDelegate: EmojiPickerCollectionViewDelegate?
private var recentEmoji: [EmojiWithSkinTones] = []
@ -70,12 +67,7 @@ class EmojiPickerCollectionView: UICollectionView {
tapGestureRecognizer.delegate = self
Storage.read { transaction in
let rawRecentEmoji = transaction.object(
forKey: EmojiPickerCollectionView.recentEmojiKey,
inCollection: EmojiPickerCollectionView.emojiPickerCollection
) as? [String] ?? []
self.recentEmoji = rawRecentEmoji.compactMap { EmojiWithSkinTones(rawValue: $0) }
self.recentEmoji = Storage.shared.getRecentEmoji(transaction: transaction)
// Some emoji have two different code points but identical appearances. Let's remove them!
// If we normalize to a different emoji than the one currently in our array, we want to drop
@ -140,29 +132,6 @@ class EmojiPickerCollectionView: UICollectionView {
return category.localizedName
}
func recordRecentEmoji(_ emoji: EmojiWithSkinTones, transaction: YapDatabaseReadWriteTransaction) {
guard recentEmoji.first != emoji else { return }
guard emoji.isNormalized else {
recordRecentEmoji(emoji.normalized, transaction: transaction)
return
}
var newRecentEmoji = recentEmoji
// Remove any existing entries for this emoji
newRecentEmoji.removeAll { emoji == $0 }
// Insert the selected emoji at the start of the list
newRecentEmoji.insert(emoji, at: 0)
// Truncate the recent emoji list to a maximum of 50 stored
newRecentEmoji = Array(newRecentEmoji[0..<min(50, newRecentEmoji.count)])
transaction.setObject(
newRecentEmoji.map { $0.rawValue },
forKey: EmojiPickerCollectionView.recentEmojiKey,
inCollection: EmojiPickerCollectionView.emojiPickerCollection
)
}
// MARK: - Search
func searchWithText(_ searchText: String?) {
@ -205,7 +174,7 @@ class EmojiPickerCollectionView: UICollectionView {
if let emoji = emoji {
Storage.write { transaction in
self.recordRecentEmoji(emoji, transaction: transaction)
Storage.shared.recordRecentEmoji(emoji, transaction: transaction)
emoji.baseEmoji.setPreferredSkinTones(emoji.skinTones, transaction: transaction)
}
@ -248,7 +217,7 @@ extension EmojiPickerCollectionView: UICollectionViewDelegate {
}
Storage.write { transaction in
self.recordRecentEmoji(emoji, transaction: transaction)
Storage.shared.recordRecentEmoji(emoji, transaction: transaction)
}
pickerDelegate?.emojiPicker(self, didSelectEmoji: emoji)
@ -378,18 +347,3 @@ private class EmojiSectionHeader: UICollectionReusableView {
return labelSize
}
}
fileprivate extension EmojiWithSkinTones {
var normalized: EmojiWithSkinTones {
switch (baseEmoji, skinTones) {
case (let base, nil) where base.normalized != base:
return EmojiWithSkinTones(baseEmoji: base.normalized)
default:
return self
}
}
var isNormalized: Bool { self == normalized }
}

@ -21,7 +21,7 @@ final class ReactionContainerView : UIView {
private var showNumbers = true
private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6
var reactions: [(String, (Int, Bool))] = []
var reactions: [(EmojiWithSkinTones, (Int, Bool))] = []
var reactionViews: [ReactionButton] = []
var expandButton: ExpandingReactionButton?
var collapseButton: UIStackView = {
@ -58,7 +58,7 @@ final class ReactionContainerView : UIView {
mainStackView.pin(to: self)
}
public func update(_ reactions: [(String, (Int, Bool))], isOutgoingMessage: Bool, showNumbers: Bool) {
public func update(_ reactions: [(EmojiWithSkinTones, (Int, Bool))], isOutgoingMessage: Bool, showNumbers: Bool) {
self.reactions = reactions
self.isOutgoingMessage = isOutgoingMessage
self.showNumbers = showNumbers
@ -70,7 +70,7 @@ final class ReactionContainerView : UIView {
}
}
private func updateCollapsedReactions(_ reactions: [(String, (Int, Bool))]) {
private func updateCollapsedReactions(_ reactions: [(EmojiWithSkinTones, (Int, Bool))]) {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = Values.smallSpacing
@ -83,8 +83,8 @@ final class ReactionContainerView : UIView {
reactionContainerView.semanticContentAttribute = .unspecified
}
var displayedReactions: [(String, (Int, Bool))]
var expandButtonReactions: [String]
var displayedReactions: [(EmojiWithSkinTones, (Int, Bool))]
var expandButtonReactions: [EmojiWithSkinTones]
if reactions.count > maxEmojisPerLine {
displayedReactions = Array(reactions[0...(maxEmojisPerLine - 3)])
@ -111,7 +111,7 @@ final class ReactionContainerView : UIView {
private func updateAllReactions() {
var reactions = self.reactions
while reactions.count > 0 {
var line: [(String, (Int, Bool))] = []
var line: [(EmojiWithSkinTones, (Int, Bool))] = []
while reactions.count > 0 && line.count < maxEmojisPerLine {
line.append(reactions.removeFirst())
}

@ -1,7 +1,7 @@
import UIKit
final class ReactionButton : UIView {
let emoji: String
let emoji: EmojiWithSkinTones
let number: Int
let showBorder: Bool
let showNumber: Bool
@ -13,7 +13,7 @@ final class ReactionButton : UIView {
private var spacing: CGFloat = Values.verySmallSpacing
// MARK: Lifecycle
init(emoji: String, value: Int, showBorder: Bool = false, showNumber: Bool = true) {
init(emoji: EmojiWithSkinTones, value: Int, showBorder: Bool = false, showNumber: Bool = true) {
self.emoji = emoji
self.number = value
self.showBorder = showBorder
@ -32,7 +32,7 @@ final class ReactionButton : UIView {
private func setUpViewHierarchy() {
let emojiLabel = UILabel()
emojiLabel.text = emoji
emojiLabel.text = emoji.rawValue
emojiLabel.font = .systemFont(ofSize: fontSize)
let stackView = UIStackView(arrangedSubviews: [ emojiLabel ])
@ -63,14 +63,14 @@ final class ReactionButton : UIView {
}
final class ExpandingReactionButton: UIView {
private let emojis: [String]
private let emojis: [EmojiWithSkinTones]
// MARK: Settings
private let size: CGFloat = 22
private let margin: CGFloat = 15
// MARK: Lifecycle
init(emojis: [String]) {
init(emojis: [EmojiWithSkinTones]) {
self.emojis = emojis
super.init(frame: CGRect.zero)
setUpViewHierarchy()
@ -96,7 +96,7 @@ final class ExpandingReactionButton: UIView {
container.layer.borderColor = isDarkMode ? UIColor.black.cgColor : UIColor.white.cgColor
let emojiLabel = UILabel()
emojiLabel.text = emoji
emojiLabel.text = emoji.rawValue
emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
container.addSubview(emojiLabel)

@ -80,6 +80,6 @@ protocol MessageCellDelegate : ReactionDelegate {
func openURL(_ url: URL)
func handleReplyButtonTapped(for viewItem: ConversationViewItem)
func showUserDetails(for sessionID: String)
func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: String?)
func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: EmojiWithSkinTones?)
func needsLayout()
}

@ -457,9 +457,9 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
}
private func populateReaction(for viewItem: ConversationViewItem, message: TSMessage) {
let reactions: OrderedDictionary<String, (Int, Bool)> = OrderedDictionary()
let reactions: OrderedDictionary<EmojiWithSkinTones, (Int, Bool)> = OrderedDictionary()
for reaction in message.reactions {
if let reactMessage = reaction as? ReactMessage, let emoji = reactMessage.emoji {
if let reactMessage = reaction as? ReactMessage, let rawEmoji = reactMessage.emoji, let emoji = EmojiWithSkinTones(rawValue: rawEmoji) {
let isSelfSend = (reactMessage.sender! == getUserHexEncodedPublicKey())
if let value = reactions.value(forKey: emoji) {
reactions.replace(key: emoji, value: (value.0 + 1, value.1 || isSelfSend))

@ -3,8 +3,8 @@ final class ReactionListSheet : BaseVC {
private let thread: TSGroupThread
private let viewItem: ConversationViewItem
private var reactions: [ReactMessage] = []
private var reactionMap: OrderedDictionary<String, [ReactMessage]> = OrderedDictionary()
var selectedReaction: String?
private var reactionMap: OrderedDictionary<EmojiWithSkinTones, [ReactMessage]> = OrderedDictionary()
var selectedReaction: EmojiWithSkinTones?
var delegate: ReactionDelegate?
// MARK: Components
@ -151,7 +151,7 @@ final class ReactionListSheet : BaseVC {
self.reactions = message.reactions as! [ReactMessage]
}
for reaction in reactions {
if let emoji = reaction.emoji {
if let rawEmoji = reaction.emoji, let emoji = EmojiWithSkinTones(rawValue: rawEmoji) {
if !reactionMap.hasValue(forKey: emoji) { reactionMap.append(key: emoji, value: []) }
var value = reactionMap.value(forKey: emoji)!
if reaction.sender == getUserHexEncodedPublicKey() {
@ -170,7 +170,7 @@ final class ReactionListSheet : BaseVC {
private func reloadData() {
reactionContainer.reloadData()
let seletedData = reactionMap.value(forKey: selectedReaction!)!
detailInfoLabel.text = "\(selectedReaction!) · \(seletedData.count)"
detailInfoLabel.text = "\(selectedReaction!.rawValue) · \(seletedData.count)"
if thread.isOpenGroup, let threadId = thread.uniqueId, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) {
let isUserModerator = OpenGroupAPIV2.isUserModerator(getUserHexEncodedPublicKey(), for: openGroupV2.room, on: openGroupV2.server)
clearAllButton.isHidden = !isUserModerator
@ -227,7 +227,7 @@ extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegat
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell
let item = reactionMap.orderedItems[indexPath.item]
cell.data = (item.0, item.1.count)
cell.data = (item.0.rawValue, item.1.count)
cell.isCurrentSelection = item.0 == selectedReaction!
return cell
}
@ -350,8 +350,8 @@ extension ReactionListSheet {
protocol ReactionDelegate : AnyObject {
func quickReact(_ viewItem: ConversationViewItem, with emoji: String)
func cancelReact(_ viewItem: ConversationViewItem, for emoji: String)
func quickReact(_ viewItem: ConversationViewItem, with emoji: EmojiWithSkinTones)
func cancelReact(_ viewItem: ConversationViewItem, for emoji: EmojiWithSkinTones)
func cancelAllReact(reactMessages: [ReactMessage])
}

@ -26,6 +26,17 @@ public struct EmojiWithSkinTones: Hashable {
return baseEmoji.rawValue
}
}
var normalized: EmojiWithSkinTones {
switch (baseEmoji, skinTones) {
case (let base, nil) where base.normalized != base:
return EmojiWithSkinTones(baseEmoji: base.normalized)
default:
return self
}
}
var isNormalized: Bool { self == normalized }
}
extension Emoji {

@ -0,0 +1,36 @@
extension Storage {
private static let emojiPickerCollection = "EmojiPickerCollection"
private static let recentEmojiKey = "recentEmoji"
func getRecentEmoji(transaction: YapDatabaseReadTransaction) -> [EmojiWithSkinTones] {
var rawRecentEmoji = transaction.object(forKey: Self.recentEmojiKey, inCollection: Self.emojiPickerCollection) as? [String] ?? []
let defaultEmoji = ["🙈", "🙉", "🙊", "😈", "🥸", "🐀"].filter{ !rawRecentEmoji.contains($0) }
if rawRecentEmoji.count < 6 {
rawRecentEmoji.append(contentsOf: defaultEmoji[..<(defaultEmoji.count - rawRecentEmoji.count)])
}
return rawRecentEmoji.compactMap { EmojiWithSkinTones(rawValue: $0) }
}
func recordRecentEmoji(_ emoji: EmojiWithSkinTones, transaction: YapDatabaseReadWriteTransaction) {
let recentEmoji = getRecentEmoji(transaction: transaction)
guard recentEmoji.first != emoji else { return }
guard emoji.isNormalized else {
recordRecentEmoji(emoji.normalized, transaction: transaction)
return
}
var newRecentEmoji = recentEmoji
// Remove any existing entries for this emoji
newRecentEmoji.removeAll { emoji == $0 }
// Insert the selected emoji at the start of the list
newRecentEmoji.insert(emoji, at: 0)
// Truncate the recent emoji list to a maximum of 50 stored
newRecentEmoji = Array(newRecentEmoji[0..<min(50, newRecentEmoji.count)])
transaction.setObject(newRecentEmoji.map { $0.rawValue }, forKey: Self.recentEmojiKey, inCollection: Self.emojiPickerCollection)
}
}

@ -33,10 +33,6 @@ public enum SNUserDefaults {
public enum String : Swift.String {
case deviceToken
}
public enum Array : Swift.String {
case recentlyUsedEmojis
}
}
public extension UserDefaults {
@ -65,29 +61,4 @@ public extension UserDefaults {
get { return self.string(forKey: string.rawValue) }
set { set(newValue, forKey: string.rawValue) }
}
subscript(array: SNUserDefaults.Array) -> [String] {
get { return self.stringArray(forKey: array.rawValue) ?? []}
set { set(newValue, forKey: array.rawValue) }
}
func getRecentlyUsedEmojis() -> [String] {
let result = self[.recentlyUsedEmojis]
if result.isEmpty {
return ["🙈", "🙉", "🙊", "😈", "🥸", "🐀"]
}
return result
}
func addNewRecentlyUsedEmoji(_ emoji: String) {
var recentlyUsedEmojis = getRecentlyUsedEmojis()
if let index = recentlyUsedEmojis.firstIndex(of: emoji) {
recentlyUsedEmojis.remove(at: index)
}
if recentlyUsedEmojis.count >= 6 {
recentlyUsedEmojis.remove(at: 5)
}
recentlyUsedEmojis.insert(emoji, at: 0)
self[.recentlyUsedEmojis] = recentlyUsedEmojis
}
}

Loading…
Cancel
Save