diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c7953fdac..46215da68 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -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 = ""; }; 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerCollectionView.swift; sourceTree = ""; }; 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiSkinTonePicker.swift; sourceTree = ""; }; + 7B1B52E1286030DF006069F2 /* Storage+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Emoji.swift"; sourceTree = ""; }; 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = ""; }; 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = ""; }; 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = ""; }; @@ -2160,6 +2162,7 @@ 7B9F71CD2852EEE2006DFE7B /* Emoji+SkinTones.swift */, 7B9F71CE2852EEE2006DFE7B /* Emoji.swift */, 7B9F71CF2852EEE2006DFE7B /* Emoji+Name.swift */, + 7B1B52E1286030DF006069F2 /* Storage+Emoji.swift */, ); path = Emoji; sourceTree = ""; @@ -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 */, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 4b324c522..bbd8e53f4 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -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() } diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift index 031c216e2..9c1e00d0b 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -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) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 45c2c9f12..206901b28 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -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) } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index b16e8871b..e97fbb2dd 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -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: { diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index 5769e7916..6eea609b1 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -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.. 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()) } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 3c1c6cd8d..1276d5c09 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -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) diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 03bac3d15..f8a5169a0 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -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() } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index f92f06979..3e33d4c4a 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -457,9 +457,9 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } private func populateReaction(for viewItem: ConversationViewItem, message: TSMessage) { - let reactions: OrderedDictionary = OrderedDictionary() + let reactions: OrderedDictionary = 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)) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index fb688a698..e407a23e1 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -3,8 +3,8 @@ final class ReactionListSheet : BaseVC { private let thread: TSGroupThread private let viewItem: ConversationViewItem private var reactions: [ReactMessage] = [] - private var reactionMap: OrderedDictionary = OrderedDictionary() - var selectedReaction: String? + private var reactionMap: OrderedDictionary = 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]) } diff --git a/Session/Emoji/EmojiWithSkinTones.swift b/Session/Emoji/EmojiWithSkinTones.swift index 26b1a14be..6c62ff255 100644 --- a/Session/Emoji/EmojiWithSkinTones.swift +++ b/Session/Emoji/EmojiWithSkinTones.swift @@ -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 { diff --git a/Session/Emoji/Storage+Emoji.swift b/Session/Emoji/Storage+Emoji.swift new file mode 100644 index 000000000..35db167db --- /dev/null +++ b/Session/Emoji/Storage+Emoji.swift @@ -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.. [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 - } }