damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit ae2f48484acfe55c2851e39ba2c7b486135c1b21
parent 2c9b280a04bd476be4bc39180fd0346dd6490110
Author: Terry Yiu <git@tyiu.xyz>
Date:   Sat, 20 Apr 2024 14:27:24 -0400

Change reactions to use a native looking emoji picker

Changelog-Changed: Change reactions to use a native looking emoji picker
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 25+++++++++++++++++--------
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 9+++++++++
Mdamus/Views/ActionBar/EventActionBar.swift | 138++++++++++++++++++-------------------------------------------------------------
Ddamus/Views/Settings/AddEmojiView.swift | 48------------------------------------------------
Ddamus/Views/Settings/EmojiListItemView.swift | 81-------------------------------------------------------------------------------
Mdamus/Views/Settings/ReactionsSettingsView.swift | 109++++++++++---------------------------------------------------------------------
6 files changed, 70 insertions(+), 340 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; }; 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; }; 3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; }; + 3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */; }; 3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; }; 4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; }; 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; }; @@ -444,8 +445,6 @@ BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; }; BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; }; BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; }; - BA4AB0AE2A63B9270070A32A /* AddEmojiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */; }; - BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */; }; BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; @@ -1368,8 +1367,6 @@ BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; }; BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; }; BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; }; - BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = "<group>"; }; - BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = "<group>"; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; }; BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; }; D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; }; @@ -1473,6 +1470,7 @@ 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */, 4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */, + 3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1727,8 +1725,6 @@ 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */, E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */, 5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */, - BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */, - BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */, ); path = Settings; sourceTree = "<group>"; @@ -2831,6 +2827,7 @@ 4C649880286E0EE300EAE2B3 /* secp256k1 */, 4C06670328FC7EC500038D2A /* Kingfisher */, 4C27C9312A64766F007DBC75 /* MarkdownUI */, + 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -2968,6 +2965,7 @@ 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */, 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, + 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -3137,7 +3135,6 @@ F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */, 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */, 4C7FF7D52823313F009601DB /* Mentions.swift in Sources */, - BA4AB0AE2A63B9270070A32A /* AddEmojiView.swift in Sources */, 4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */, 4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */, 4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */, @@ -3180,7 +3177,6 @@ 4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */, 4C7D09602A098C5D00943473 /* WalletView.swift in Sources */, 4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */, - BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */, B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */, 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, 4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */, @@ -4268,6 +4264,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/izyumkin/MCEmojiPicker"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.2.3; + }; + }; 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher"; @@ -4311,6 +4315,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */ = { + isa = XCSwiftPackageProductDependency; + package = 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */; + productName = MCEmojiPicker; + }; 4C06670328FC7EC500038D2A /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,6 +19,15 @@ } }, { + "identity" : "mcemojipicker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/izyumkin/MCEmojiPicker", + "state" : { + "revision" : "e0b4903b75ae1cc418d276d84d1cb946b8a1d73c", + "version" : "1.2.3" + } + }, + { "identity" : "secp256k1.swift", "kind" : "remoteSourceControl", "location" : "https://github.com/jb55/secp256k1.swift", diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -6,8 +6,7 @@ // import SwiftUI -import UIKit - +import MCEmojiPicker struct EventActionBar: View { let damus_state: DamusState @@ -20,6 +19,8 @@ struct EventActionBar: View { @State var show_share_action: Bool = false @State var show_repost_action: Bool = false + @State private var isOnTopHalfOfScreen: Bool = false + @ObservedObject var bar: ActionBarModel init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) { @@ -72,7 +73,7 @@ struct EventActionBar: View { Spacer() HStack(spacing: 4) { - LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil) { emoji in + LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil, isOnTopHalfOfScreen: $isOnTopHalfOfScreen) { emoji in if bar.liked { //notify(.delete, bar.our_like) } else { @@ -135,8 +136,22 @@ struct EventActionBar: View { self.bar.our_like = liked.event } } + .background( + GeometryReader { geometry in + EmptyView() + .onAppear { + let eventActionBarY = geometry.frame(in: .global).midY + let screenMidY = UIScreen.main.bounds.midY + self.isOnTopHalfOfScreen = eventActionBarY > screenMidY + } + .onChange(of: geometry.frame(in: .global).midY) { newY in + let screenMidY = UIScreen.main.bounds.midY + self.isOnTopHalfOfScreen = newY > screenMidY + } + } + ) } - + func send_like(emoji: String) { guard let keypair = damus_state.keypair.to_full(), let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else { @@ -168,15 +183,17 @@ struct LikeButton: View { let damus_state: DamusState let liked: Bool let liked_emoji: String? + @Binding var isOnTopHalfOfScreen: Bool let action: (_ emoji: String) -> Void // For reactions background @State private var showReactionsBG = 0 - @State private var showEmojis: [Int] = [] @State private var rotateThumb = -45 @State private var isReactionsVisible = false + @State private var selectedEmoji: String = "" + // Following four are Shaka animation properties let timer = Timer.publish(every: 0.10, on: .main, in: .common).autoconnect() @State private var shouldAnimate = false @@ -228,7 +245,15 @@ struct LikeButton: View { amountOfAngleIncrease = 20.0 } }) - .overlay(reactionsOverlay()) + .emojiPicker( + isPresented: $isReactionsVisible, + selectedEmoji: $selectedEmoji, + arrowDirection: isOnTopHalfOfScreen ? .down : .up, + isDismissAfterChoosing: true + ) + .onChange(of: selectedEmoji) { newSelectedEmoji in + self.action(newSelectedEmoji) + } } func shakaAnimationLogic() { @@ -251,110 +276,11 @@ struct LikeButton: View { } } - func reactionsOverlay() -> some View { - Group { - if isReactionsVisible { - ZStack { - RoundedRectangle(cornerRadius: 20) - .frame(width: calculateOverlayWidth(), height: 50) - .foregroundColor(DamusColors.black) - .scaleEffect(Double(showReactionsBG), anchor: .topTrailing) - .animation( - .interpolatingSpring(stiffness: 170, damping: 15).delay(0.05), - value: showReactionsBG - ) - .overlay( - Rectangle() - .foregroundColor(Color.white.opacity(0.2)) - .frame(width: calculateOverlayWidth(), height: 50) - .clipShape( - RoundedRectangle(cornerRadius: 20) - ) - ) - .overlay(reactions()) - } - .offset(y: -40) - .onTapGesture { - withAnimation(.easeOut(duration: 0.2)) { - isReactionsVisible = false - showReactionsBG = 0 - } - showEmojis = [] - } - } else { - EmptyView() - } - } - } - - func calculateOverlayWidth() -> CGFloat { - let maxWidth: CGFloat = 250 - let numberOfEmojis = emojis.count - let minimumWidth: CGFloat = 75 - - if numberOfEmojis > 0 { - let emojiWidth: CGFloat = 25 - let padding: CGFloat = 15 - let buttonWidth: CGFloat = 18 - let buttonPadding: CGFloat = 20 - - let totalWidth = CGFloat(numberOfEmojis) * (emojiWidth + padding) + buttonWidth + buttonPadding - return min(maxWidth, max(minimumWidth, totalWidth)) - } else { - return minimumWidth - } - } - - func reactions() -> some View { - HStack { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 15) { - ForEach(emojis, id: \.self) { emoji in - if let index = emojis.firstIndex(of: emoji) { - let scale = index < showEmojis.count ? showEmojis[index] : 0 - Text(emoji) - .font(.system(size: 25)) - .scaleEffect(Double(scale)) - .onTapGesture { - emojiTapped(emoji) - } - } - } - } - .padding(.leading, 10) - } - Button(action: { - withAnimation(.easeOut(duration: 0.2)) { - isReactionsVisible = false - showReactionsBG = 0 - } - showEmojis = [] - }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 18)) - .foregroundColor(.gray) - } - .padding(.trailing, 7.5) - } - } - // When reaction button is long pressed, it displays the multiple emojis overlay and displays the user's selected emojis with an animation private func reactionLongPressed() { UIImpactFeedbackGenerator(style: .medium).impactOccurred() - showEmojis = Array(repeating: 0, count: emojis.count) // Initialize the showEmojis array - - for (index, _) in emojis.enumerated() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1 * Double(index)) { - withAnimation(.interpolatingSpring(stiffness: 170, damping: 8)) { - if index < showEmojis.count { - showEmojis[index] = 1 - } - } - } - } isReactionsVisible = true - showReactionsBG = 1 } private func emojiTapped(_ emoji: String) { @@ -364,9 +290,7 @@ struct LikeButton: View { withAnimation(.easeOut(duration: 0.2)) { isReactionsVisible = false - showReactionsBG = 0 } - showEmojis = [] withAnimation(Animation.easeOut(duration: 0.15)) { shouldAnimate = true diff --git a/damus/Views/Settings/AddEmojiView.swift b/damus/Views/Settings/AddEmojiView.swift @@ -1,48 +0,0 @@ -// -// AddEmojiView.swift -// damus -// -// Created by Suhail Saqan on 7/16/23. -// - -import SwiftUI - -struct AddEmojiView: View { - @Binding var emoji: String - - var body: some View { - ZStack(alignment: .leading) { - HStack{ - TextField(NSLocalizedString("⚡", comment: "Placeholder example for an emoji reaction"), text: $emoji) - .padding(2) - .padding(.leading, 25) - .opacity(emoji == "" ? 0.5 : 1) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - .onChange(of: emoji) { newEmoji in - if let lastEmoji = newEmoji.last.map(String.init), isValidEmoji(lastEmoji) { - self.emoji = lastEmoji - } else { - self.emoji = "" - } - } - - Label("", image: "close-circle") - .foregroundColor(.accentColor) - .padding(.trailing, -25.0) - .opacity((emoji == "") ? 0.0 : 1.0) - .onTapGesture { - self.emoji = "" - } - } - - Label("", image: "copy2") - .padding(.leading, -10) - .onTapGesture { - if let pastedEmoji = UIPasteboard.general.string { - self.emoji = pastedEmoji - } - } - } - } -} diff --git a/damus/Views/Settings/EmojiListItemView.swift b/damus/Views/Settings/EmojiListItemView.swift @@ -1,81 +0,0 @@ -// -// EmojiListItemView.swift -// damus -// -// Created by Suhail Saqan on 7/16/23. -// - -import SwiftUI - -struct EmojiListItemView: View { - @ObservedObject var settings: UserSettingsStore - - let emoji: String - let recommended: Bool - - @Binding var showActionButtons: Bool - - var body: some View { - Group { - HStack { - if showActionButtons { - if recommended { - AddButton() - } else { - RemoveButton() - } - } - - Text(emoji) - } - } - .swipeActions { - if !recommended { - RemoveButton() - .tint(.red) - } else { - AddButton() - .tint(.green) - } - } - .contextMenu { - if !showActionButtons { - CopyAction(emoji: emoji) - } - } - } - - func CopyAction(emoji: String) -> some View { - Button { - UIPasteboard.general.setValue(emoji, forPasteboardType: "public.plain-text") - } label: { - Label(NSLocalizedString("Copy", comment: "Button to copy an emoji reaction"), image: "copy2") - } - } - - func RemoveButton() -> some View { - Button(action: { - if let index = settings.emoji_reactions.firstIndex(of: emoji) { - settings.emoji_reactions.remove(at: index) - } - }) { - Image(systemName: "minus.circle") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(.red) - .padding(.leading, -5) - } - } - - func AddButton() -> some View { - Button(action: { - settings.emoji_reactions.append(emoji) - }) { - Image(systemName: "plus.circle") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(.green) - .padding(.leading, -5) - } - } -} diff --git a/damus/Views/Settings/ReactionsSettingsView.swift b/damus/Views/Settings/ReactionsSettingsView.swift @@ -6,114 +6,31 @@ // import SwiftUI -import Combine +import MCEmojiPicker struct ReactionsSettingsView: View { @ObservedObject var settings: UserSettingsStore - - @State var new_emoji: String = "" - @State private var showActionButtons = false - - @Environment(\.dismiss) var dismiss - - var recommended: [String] { - return getMissingRecommendedEmojis(added: settings.emoji_reactions) - } - + @State private var isReactionsVisible: Bool = false + var body: some View { Form { Section { - AddEmojiView(emoji: $new_emoji) - } header: { - Text(NSLocalizedString("Add Emoji", comment: "Label for section for adding an emoji to the reactions list.")) - .font(.system(size: 18, weight: .heavy)) - .padding(.bottom, 5) - } footer: { - HStack { - Spacer() - if !new_emoji.isEmpty { - Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted emoji.")) { - new_emoji = "" - } - .font(.system(size: 14, weight: .bold)) - .frame(width: 80, height: 30) - .foregroundColor(.white) - .background(LINEAR_GRADIENT) - .clipShape(Capsule()) - .padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0)) - - Button(NSLocalizedString("Add", comment: "Button to confirm adding user inputted emoji.")) { - if isValidEmoji(new_emoji) { - settings.emoji_reactions.append(new_emoji) - new_emoji = "" - } - } - .font(.system(size: 14, weight: .bold)) - .frame(width: 80, height: 30) - .foregroundColor(.white) - .background(LINEAR_GRADIENT) - .clipShape(Capsule()) - .padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0)) - } - } - } - - Picker(NSLocalizedString("Select default emoji", comment: "Prompt selection of user's default emoji reaction"), - selection: $settings.default_emoji_reaction) { - ForEach(settings.emoji_reactions, id: \.self) { emoji in - Text(emoji) - } - } - - Section { - List { - ForEach(Array(zip(settings.emoji_reactions, 1...)), id: \.1) { tup in - EmojiListItemView(settings: settings, emoji: tup.0, recommended: false, showActionButtons: $showActionButtons) + Text(settings.default_emoji_reaction) + .emojiPicker( + isPresented: $isReactionsVisible, + selectedEmoji: $settings.default_emoji_reaction, + arrowDirection: .up, + isDismissAfterChoosing: true + ) + .onTapGesture { + isReactionsVisible = true } - .onMove(perform: showActionButtons ? move: nil) - } } header: { - Text("Emoji Reactions", comment: "Section title for emoji reactions that are currently added.") - .font(.system(size: 18, weight: .heavy)) - .padding(.bottom, 5) - } - - if recommended.count > 0 { - Section { - List(Array(zip(recommended, 1...)), id: \.1) { tup in - EmojiListItemView(settings: settings, emoji: tup.0, recommended: true, showActionButtons: $showActionButtons) - } - } header: { - Text("Recommended Emojis", comment: "Section title for recommend emojis") - .font(.system(size: 18, weight: .heavy)) - .padding(.bottom, 5) - } + Text(NSLocalizedString("Select default emoji", comment: "Prompt selection of user's default emoji reaction")) } } .navigationTitle(NSLocalizedString("Reactions", comment: "Title of emoji reactions view")) .navigationBarTitleDisplayMode(.large) - .toolbar { - if showActionButtons { - Button("Done") { - showActionButtons.toggle() - } - } else { - Button("Edit") { - showActionButtons.toggle() - } - } - } - } - - private func move(from: IndexSet, to: Int) { - settings.emoji_reactions.move(fromOffsets: from, toOffset: to) - } - - // Returns the emojis that are in the recommended list but the user has not added yet - func getMissingRecommendedEmojis(added: [String], recommended: [String] = default_emoji_reactions) -> [String] { - let addedSet = Set(added) - let missingEmojis = recommended.filter { !addedSet.contains($0) } - return missingEmojis } }