conversation view: add default emoji palette

Gitlab: #326
Change-Id: I08cdc114445f8ce2abbc5f16f942d9e9c986986f
diff --git a/Ring/Ring/Extensions/String+Helpers.swift b/Ring/Ring/Extensions/String+Helpers.swift
index e5058d9..b79bdf5 100644
--- a/Ring/Ring/Extensions/String+Helpers.swift
+++ b/Ring/Ring/Extensions/String+Helpers.swift
@@ -167,7 +167,7 @@
             .replacingOccurrences(of: ")", with: "")
     }
 
-    func containsCaseInsentative(string: String) -> Bool {
+    func containsCaseInsensitive(string: String) -> Bool {
         return self.range(of: string, options: .caseInsensitive) != nil
     }
 }
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
index f5092e0..228e2fa 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
@@ -173,7 +173,7 @@
                      can receive taps. The tap gesture should be re-added
                      once the contact picker is dismissed.
                      */
-                        self.view.removeGestureRecognizer(self.screenTapRecognizer)
+                    self.view.removeGestureRecognizer(self.screenTapRecognizer)
                     self.viewModel.slectContactsToShareMessage(message: message)
                 case .share(let items):
                     self.presentActivityControllerWithItems(items: items)
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/ContextMenuVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/ContextMenuVM.swift
index a04255a..6ebfa09 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/ContextMenuVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/ContextMenuVM.swift
@@ -20,8 +20,10 @@
 
 import Foundation
 import SwiftUI
+import RxRelay
 
 class ContextMenuVM {
+    var sendEmojiUpdate = BehaviorRelay(value: [String: String]())
     @Published var menuItems = [ContextualMenuItem]()
     var presentingMessage: MessageBubbleView! {
         didSet {
@@ -29,6 +31,7 @@
             actionsAnchor = presentingMessage.model.message.incoming ? .topLeading : .topTrailing
             messsageAnchor = presentingMessage.model.message.incoming ? .bottomLeading : .bottomTrailing
             updateContextMenuSize()
+            isOurMsg = !presentingMessage.model.message.incoming
         }
     }
     var messageFrame: CGRect = CGRect.zero {
@@ -49,17 +52,32 @@
     let menuItemFont = Font.callout
     let screenPadding: CGFloat = 100
     let menuCornerRadius: CGFloat = 3
+    let defaultVerticalPadding: CGFloat = 6
+    let maxScaleFactor: CGFloat = 1.1
     var bottomOffset: CGFloat = 0 // move message up
     var menuOffsetX: CGFloat = 0
     var menuOffsetY: CGFloat = 0
     var scaleMessageUp = true
     var actionsAnchor: UnitPoint = .center
     var messsageAnchor: UnitPoint = .center
-    var messageHeight: CGFloat = 0
+    var messageHeight: CGFloat = 0 {
+        didSet {
+            isShortMsg = messageHeight < screenHeight / 4.0
+        }
+    }
+    var emojiVerticalPadding: CGFloat = 6
+
+    var emojiBarHeight: CGFloat = 68
+    var isShortMsg: Bool = true
+    var incomingMessageMarginSize: CGFloat = 58
+    var isOurMsg: Bool?
+
     var shadowColor: UIColor {
         return UITraitCollection.current.userInterfaceStyle == .light ? UIColor.tertiaryLabel : UIColor.black.withAlphaComponent(0.8)
     }
 
+    var currentJamiAccountId: String?
+
     func updateContextMenuSize() {
         let height: CGFloat = CGFloat(menuItems.count) * itemHeight + menuPadding * 2
         let fontAttributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .callout)]
@@ -93,5 +111,49 @@
         }
         let difff = messageFrame.height + navBarHeight + menuSize.height - screenHeight
         scaleMessageUp = difff <= 0
+        if scaleMessageUp {
+            let heightDiff = messageHeight * maxScaleFactor - messageHeight
+            /*
+             Because the messageAnchor for the scale is at the bottom,
+             the message will expand upwards. Therefore, it is necessary
+             to add scaled space.
+             */
+            emojiVerticalPadding = defaultVerticalPadding + heightDiff
+        }
+        // set the left margin for incoming messages when reactions are opened
+        incomingMessageMarginSize = messageFrame.minX
+    }
+
+    func sendReaction(value: String) {
+        if let msg = self.presentingMessage {
+            self.sendEmojiUpdate.accept(["messageId": msg.messageModel.id, "author": msg.messageModel.message.authorId, "data": value, "action": ReactionCommand.apply.toString()])
+        }
+    }
+
+    func revokeReaction(value: String, reactionId: String) {
+        if let msg = self.presentingMessage {
+            self.sendEmojiUpdate.accept(["reactionId": reactionId, "author": msg.messageModel.message.authorId, "data": value, "action": ReactionCommand.revoke.toString()])
+        }
+    }
+
+    func localUserAuthoredReaction(emoji: String) -> Bool {
+        if let sender = self.currentJamiAccountId {
+            return self.presentingMessage.messageModel.message.reactions.first(where: { item in item.author == sender && item.content == emoji }) != nil
+        }
+        return false
+    }
+}
+
+enum ReactionCommand {
+    case apply
+    case revoke
+
+    func toString() -> String {
+        switch self {
+        case .apply:
+            return "apply"
+        case .revoke:
+            return "revoke"
+        }
     }
 }
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageContainerModel.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageContainerModel.swift
index dd5bd28..6823558 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageContainerModel.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageContainerModel.swift
@@ -136,7 +136,7 @@
         DispatchQueue.main.async { [weak self, weak image] in
             guard let self = self, let image = image else { return }
             if self.messageRow.shouldDisplayAavatar && self.message.incoming {
-                self.messageRow.avatarImage = image
+                self.messageRow.updateImage(image: image, jamiId: jamiId)
             }
             if self.message.type == .contact && self.message.incoming {
                 self.contactViewModel.avatarImage = image
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageContentVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageContentVM.swift
index c1fdbd9..d0f76ac 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageContentVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageContentVM.swift
@@ -443,9 +443,6 @@
     }
 
     func swarmColorUpdated(color: UIColor) {
-        if self.message.incoming || self.content.containsOnlyEmoji {
-            return
-        }
         DispatchQueue.main.async { [weak self] in
             guard let self = self else { return }
             self.preferencesColor = color
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageRowVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageRowVM.swift
index 84d5598..d4222dc 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageRowVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessageRowVM.swift
@@ -57,6 +57,13 @@
         }
     }
 
+    func updateImage(image: UIImage, jamiId: String) {
+        let localId = message.uri.isEmpty ? message.authorId : message.uri
+        if jamiId == localId {
+            self.avatarImage = image
+        }
+    }
+
     var sequencing: MessageSequencing = .unknown {
         didSet {
             topSpace = (sequencing == .singleMessage || sequencing == .firstOfSequence) ? 2 : 0
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
index 155a124..45c6df9 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
@@ -88,6 +88,7 @@
 class MessagesListVM: ObservableObject {
 
     // view properties
+    var contextMenuModel = ContextMenuVM()
     @Published var messagesModels = [MessageContainerModel]()
     @Published var scrollToId: String?
     @Published var scrollToReplyTarget: String? // message id of a reply target that we should scroll
@@ -342,6 +343,25 @@
                 self.reactionsUpdated(messageId: messageId)
             })
             .disposed(by: self.disposeBag)
+        contextMenuModel.currentJamiAccountId = self.accountService.currentAccount?.jamiId
+        // setup subscription for emoji picker
+        self.contextMenuModel.sendEmojiUpdate
+            .subscribe(onNext: { [weak self] event in
+                if let self = self, !event.isEmpty {
+                    switch event["action"] {
+                    case ReactionCommand.apply.toString():
+                        if let dat = event["data"], let mId = event["messageId"] {
+                            self.conversationService.sendEmojiReactionMessage(conversationId: self.conversation.id, accountId: self.conversation.accountId, message: dat, parentId: mId)
+                        }
+                    case ReactionCommand.revoke.toString():
+                        if let rId = event["reactionId"] {
+                            self.conversationService.editSwarmMessage(conversationId: self.conversation.id, accountId: self.conversation.accountId, message: "", parentId: rId)
+                        }
+                    default: break
+                    }
+                }
+            })
+            .disposed(by: self.disposeBag)
     }
 
     func subscribeMessageUpdates() {
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/ContextMenuView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/ContextMenuView.swift
index e59a6b5..a10357a 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/ContextMenuView.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/ContextMenuView.swift
@@ -56,48 +56,92 @@
     @SwiftUI.State private var messageOffsetDiff: CGFloat = 0
     @SwiftUI.State private var cornerRadius: CGFloat = 0
     @SwiftUI.State private var scrollViewHeight: CGFloat = 0
+    @SwiftUI.GestureState private var isLongPressingEmojiBar = false
 
     var body: some View {
         ZStack {
             GeometryReader { _ in
                 VStack(alignment: .leading) {
-                    // message
-                    ScrollView {
-                        model.presentingMessage
-                            .frame(
-                                width: model.messageFrame.width,
-                                height: model.messageFrame.height
-                            )
-                    }
-                    .cornerRadius(cornerRadius)
-                    .scaleEffect(messageScale, anchor: model.messsageAnchor)
-                    .shadow(color: Color(model.shadowColor), radius: messageShadow)
-                    .frame(
-                        width: model.messageFrame.width,
-                        height: scrollViewHeight
-                    )
                     Spacer()
-                        .frame(height: 10)
-                    // actions
-                    makeActions()
-                        .frame(width: model.menuSize.width)
-                        .opacity(actionsOpacity)
-                        .scaleEffect(actionsScale, anchor: model.actionsAnchor)
-                        .offset(
-                            x: model.menuOffsetX,
-                            y: model.menuOffsetY
-                        )
+                        .frame(height: model.defaultVerticalPadding)
+                    // emoji picker
+                    if model.isShortMsg {
+                        HStack {
+                            if !model.isOurMsg! {
+                                Spacer()
+                                    .frame(width: model.incomingMessageMarginSize)
+                            }
+                            makeEmojiBar()
+                            if model.isOurMsg! {
+                                Spacer()
+                                    .frame(width: 10)
+                            }
+                        }
+                        .frame(width: screenWidth, alignment: model.isOurMsg! ? .trailing : .leading)
+                        Spacer()
+                            .frame(height: model.emojiVerticalPadding)
+                    }
+                    // message body in scrollable view
+                    // message + tappable area
+                    HStack {
+                        if !model.isOurMsg! {
+                            Spacer()
+                                .frame(width: model.incomingMessageMarginSize)
+                        }
+                        tappableMessageBody()
+                        if model.isOurMsg! {
+                            Spacer()
+                                .frame(width: 10)
+                        }
+                    }
+                    .frame(width: screenWidth, alignment: model.isOurMsg! ? .trailing : .leading)
+                    // extra check for long messages to move emojis closer to the touch center
+                    if !model.isShortMsg {
+                        HStack {
+                            if !model.isOurMsg! {
+                                Spacer()
+                                    .frame(width: model.incomingMessageMarginSize)
+                            }
+                            makeEmojiBar()
+                            if model.isOurMsg! {
+                                Spacer()
+                                    .frame(width: 10)
+                            }
+                        }
+                        .frame(width: screenWidth, alignment: model.isOurMsg! ? .trailing : .leading)
+                    } else {
+                        Spacer()
+                            .frame(height: model.defaultVerticalPadding)
+                    }
+                    // actions (reply, fwd, etc.)
+                    HStack {
+                        if !model.isOurMsg! {
+                            Spacer()
+                                .frame(width: model.incomingMessageMarginSize)
+                        }
+                        makeActions()
+                            .opacity(actionsOpacity)
+                            .scaleEffect(actionsScale, anchor: model.actionsAnchor)
+                            .frame(width: model.menuSize.width)
+                        if model.isOurMsg! {
+                            Spacer()
+                                .frame(width: 10)
+                        }
+                    }
+                    .frame(width: screenWidth, alignment: model.isOurMsg! ? .trailing : .leading)
                 }
-                .offset(
-                    x: model.messageFrame.origin.x,
-                    y: model.messageFrame.origin.y + messageOffsetDiff
-                )
+                .padding(.trailing, 4)
             }
-            .background(makeBackground())
+            .offset( // offset vstack
+                x: 0,
+                y: max(0, model.messageFrame.origin.y + messageOffsetDiff - (model.isShortMsg ? model.emojiBarHeight : 0))
+            )
+
         }
+        .background(makeBackground())
         .onTapGesture {
             presentingState = .willDismissWithoutAction
-            withAnimation(Animation.easeOut(duration: 0.3)) {
+            withAnimation(Animation.easeOut(duration: 0.1)) {
                 scrollViewHeight = model.messageFrame.height
                 blurAmount = 0
                 backgroundScale = 1.00
@@ -109,17 +153,17 @@
                 messageOffsetDiff = 0
                 cornerRadius = 0
             }
-            DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.42) {
                 presentingState = .dismissed
             }
         }
         .onAppear(perform: {
             scrollViewHeight = model.messageFrame.height
-            withAnimation(.easeOut(duration: 0.4)) {
-                messageScale = model.scaleMessageUp ? 1.1 : 1.0
+            withAnimation(.easeOut(duration: 0.3)) {
+                messageScale = model.scaleMessageUp ? model.maxScaleFactor : 1.0
                 messageShadow = 4
             }
-            withAnimation(.easeIn(duration: 0.2).delay(0.3)) {
+            withAnimation(.easeIn(duration: 0.2).delay(0.15)) {
                 let impactMed = UIImpactFeedbackGenerator(style: .medium)
                 impactMed.impactOccurred()
                 blurAmount = 10
@@ -130,13 +174,61 @@
                 messageOffsetDiff = model.bottomOffset
                 cornerRadius = model.menuCornerRadius
             }
-            withAnimation(.spring(response: 0.2, dampingFraction: 0.6, blendDuration: 0.2).delay(0.3)) {
+            withAnimation(.spring(response: 0.2, dampingFraction: 0.6, blendDuration: 0.2).delay(0.15)) {
                 actionsScale = 1
             }
         })
         .edgesIgnoringSafeArea(.all)
     }
 
+    func tappableMessageBody() -> some View {
+        ZStack {
+            ScrollView {
+                model.presentingMessage
+                    .frame(
+                        width: model.messageFrame.width,
+                        height: model.messageFrame.height
+                    )
+
+            }
+            .cornerRadius(cornerRadius)
+            .scaleEffect(messageScale, anchor: model.messsageAnchor)
+            .shadow(color: Color(model.shadowColor), radius: messageShadow)
+            .frame(
+                width: model.messageFrame.width,
+                height: scrollViewHeight
+            )
+            // invisible tap area for accessibility
+            Rectangle()
+                .cornerRadius(cornerRadius)
+                .scaleEffect(messageScale, anchor: model.messsageAnchor)
+                .frame(
+                    width: model.messageFrame.width,
+                    height: scrollViewHeight
+                )
+                .onTapGesture {
+                    presentingState = .willDismissWithoutAction
+                    withAnimation(Animation.easeOut(duration: 0.1)) {
+                        scrollViewHeight = model.messageFrame.height
+                        blurAmount = 0
+                        backgroundScale = 1.00
+                        messageScale = 1
+                        actionsScale = 0.00
+                        actionsOpacity = 0
+                        messageShadow = 0
+                        backgroundOpacity = 0
+                        messageOffsetDiff = 0
+                        cornerRadius = 0
+                    }
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+                        presentingState = .dismissed
+                    }
+                }
+                .foregroundColor(Color.clear) // Make the Rectangle transparent
+                .contentShape(Rectangle())
+        }
+    }
+
     func makeBackground() -> some View {
         ZStack {
             Color(UIColor.systemBackground)
@@ -151,6 +243,32 @@
         .edgesIgnoringSafeArea(.all)
     }
 
+    func makeEmojiBar() -> some View {
+        HStack {
+            let defaultReactionEmojis: [String] = [
+                0x1F44D, 0x1F44E, 0x1F606, 0x1F923, 0x1F615
+            ].map { String(UnicodeScalar($0)!) }
+
+            ForEach(defaultReactionEmojis.indices, id: \.self) { index in
+                EmojiBarView(
+                    model: model,
+                    emoji: defaultReactionEmojis[index],
+                    presentingState: $presentingState,
+                    elementOpacity: 0.0 as CGFloat,
+                    delayIn: 0.03 * Double(index),
+                    elementRotation: Angle(degrees: 10.0 * Double(defaultReactionEmojis.count))
+                )
+            }
+
+        }
+        .opacity(actionsOpacity)
+        .padding(.vertical, 3)
+        .padding(.horizontal, 8)
+        .background(Color(UIColor.secondarySystemBackground))
+        .cornerRadius(radius: 32, corners: .allCorners)
+        .shadow(color: Color(model.shadowColor), radius: messageShadow)
+    }
+
     func makeActions() -> some View {
         VStack(spacing: 0) {
             ForEach(model.menuItems) { item in
@@ -193,3 +311,69 @@
         .cornerRadius(radius: model.menuCornerRadius, corners: .allCorners)
     }
 }
+
+struct EmojiBarView: View {
+    var model: ContextMenuVM
+    var emoji: String
+    @Binding var presentingState: ContextMenuPresentingState
+    @SwiftUI.State var elementOpacity: CGFloat
+    @SwiftUI.State var delayIn: Double
+    @SwiftUI.State var elementRotation: Angle
+    @SwiftUI.State private var enabledNotifierLength: CGFloat = 0
+    @SwiftUI.State private var hightligthColor: UIColor = UIColor.defaultSwarmColor
+
+    var body: some View {
+        let emojiActive = model.localUserAuthoredReaction(emoji: emoji)
+        VStack {
+            Text(verbatim: emoji)
+                .font(.title2)
+                .opacity(elementOpacity)
+                .rotationEffect(elementRotation)
+                .padding(8)
+                .overlay(
+                    Rectangle()
+                        .fill(Color(hightligthColor))
+                        .opacity(emojiActive ? elementOpacity : 0)
+                        .frame(width: enabledNotifierLength, height: 3, alignment: .center)
+                        .cornerRadius(8)
+                        .offset(y: 20)
+                        .onAppear(perform: {
+                            hightligthColor = model.presentingMessage.model.preferencesColor
+                            withAnimation(.spring(response: 0.4, dampingFraction: 0.3, blendDuration: 0.9).delay(delayIn + 0.5)) {
+                                enabledNotifierLength = 20
+                            }
+                        })
+                )
+        }
+        .simultaneousGesture(
+            // handles adding or removing the default reactions from a message
+            TapGesture().onEnded({ _ in
+                DispatchQueue.main.async {
+                    switch emojiActive {
+                    case false:
+                        model.sendReaction(value: emoji)
+                    case true:
+                        let reactionMsgId: String =
+                            model.presentingMessage.model.message.reactions.first(where: {
+                                item in item.author == model.currentJamiAccountId && item.content == emoji
+                            })!.id
+                        model.revokeReaction(value: emoji, reactionId: reactionMsgId)
+                    }
+                    presentingState = .dismissed
+                }
+            }))
+        .padding(4)
+        .onAppear {
+            withAnimation(.spring(response: 0.2, dampingFraction: 0.6, blendDuration: 0.2).delay(delayIn)) {
+                elementOpacity = 1
+            }
+            withAnimation(.spring(response: 0.2, dampingFraction: 0.6, blendDuration: 0.3).delay(delayIn)) {
+                elementRotation = Angle(degrees: elementRotation.degrees / -2)
+            }
+            withAnimation(.spring(response: 0.2, dampingFraction: 0.3, blendDuration: 0.5).delay(delayIn + 0.3)) {
+                elementRotation = Angle(degrees: 0)
+            }
+        }
+    }
+
+}
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
index baac773..4916154 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/Views/MessagesListView.swift
@@ -57,7 +57,6 @@
     @SwiftUI.State private var currentSnapshot: UIImage?
     @SwiftUI.State private var presentingMessage: MessageBubbleView?
     @SwiftUI.State private var messageFrame: CGRect?
-    var contextMenuModel = ContextMenuVM()
     @SwiftUI.State private var screenHeight: CGFloat = 0
     @SwiftUI.State private var messageContainerHeight: CGFloat = 0
     @SwiftUI.State private var shouldHideActiveKeyboard = false
@@ -89,7 +88,7 @@
                             return dimensions[VerticalAlignment.center]
                         }
                 }
-                .overlay(contextMenuPresentingState == .shouldPresent && contextMenuModel.presentingMessage != nil ? makeOverlay() : nil)
+                .overlay(contextMenuPresentingState == .shouldPresent && model.contextMenuModel.presentingMessage != nil ? makeOverlay() : nil)
                 // hide navigation bar when presenting context menu
                 .onChange(of: contextMenuPresentingState) { newValue in
                     let shouldHide = newValue == .shouldPresent
@@ -147,7 +146,7 @@
     }
 
     func makeOverlay() -> some View {
-        return ContextMenuView(model: contextMenuModel, presentingState: $contextMenuPresentingState)
+        return ContextMenuView(model: model.contextMenuModel, presentingState: $contextMenuPresentingState)
     }
 
     private func createMessagesStackView() -> some View {
@@ -213,8 +212,8 @@
                 return
             }
             model.hideNavigationBar.accept(true)
-            contextMenuModel.presentingMessage = message
-            contextMenuModel.messageFrame = frame
+            model.contextMenuModel.presentingMessage = message
+            model.contextMenuModel.messageFrame = frame
             /*
              If the keyboard is open, it should be closed.
              Once the context menu is removed, the keyboard
diff --git a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift
index 6954690..c68b83c 100644
--- a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift
+++ b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchViewModel.swift
@@ -163,13 +163,13 @@
         } else {
             var displayNameContainsText = false
             if let displayName = conversation.displayName.value {
-                displayNameContainsText = displayName.containsCaseInsentative(string: searchQuery)
+                displayNameContainsText = displayName.containsCaseInsensitive(string: searchQuery)
             }
             var participantHashContainsText = false
             if let hash = conversation.model().getParticipants().first?.jamiId {
-                participantHashContainsText = hash.containsCaseInsentative(string: searchQuery)
+                participantHashContainsText = hash.containsCaseInsensitive(string: searchQuery)
             }
-            return conversation.userName.value.containsCaseInsentative(string: searchQuery) ||
+            return conversation.userName.value.containsCaseInsensitive(string: searchQuery) ||
                 displayNameContainsText || participantHashContainsText
         }
     }
diff --git a/Ring/Ring/Services/ConversationsService.swift b/Ring/Ring/Services/ConversationsService.swift
index 2bcec60..875ceba 100644
--- a/Ring/Ring/Services/ConversationsService.swift
+++ b/Ring/Ring/Services/ConversationsService.swift
@@ -277,14 +277,18 @@
         self.conversationsAdapter.loadConversationMessages(accountId, conversationId: conversationId, from: id, size: 1)
     }
 
-    func sendSwarmMessage(conversationId: String, accountId: String, message: String, parentId: String) {
-        self.conversationsAdapter.sendSwarmMessage(accountId, conversationId: conversationId, message: message, parentId: parentId, flag: 0)
-    }
-
     func editSwarmMessage(conversationId: String, accountId: String, message: String, parentId: String) {
         self.conversationsAdapter.sendSwarmMessage(accountId, conversationId: conversationId, message: message, parentId: parentId, flag: 1)
     }
 
+    func sendEmojiReactionMessage(conversationId: String, accountId: String, message: String, parentId: String) {
+        self.conversationsAdapter.sendSwarmMessage(accountId, conversationId: conversationId, message: message, parentId: parentId, flag: 2)
+    }
+
+    func sendSwarmMessage(conversationId: String, accountId: String, message: String, parentId: String) {
+        self.conversationsAdapter.sendSwarmMessage(accountId, conversationId: conversationId, message: message, parentId: parentId, flag: 0)
+    }
+
     func insertReplies(messages: [MessageModel], accountId: String, conversationId: String, fromLoaded: Bool) -> Bool {
         if self.isTargetReply(messages: messages) {
             self.processReplyTargetMessage(with: messages.first)
diff --git a/Ring/Ring/SwarmInfo.swift b/Ring/Ring/SwarmInfo.swift
index fe0ba6d..fe42c07 100644
--- a/Ring/Ring/SwarmInfo.swift
+++ b/Ring/Ring/SwarmInfo.swift
@@ -228,10 +228,10 @@
     }
 
     func contains(searchQuery: String) -> Bool {
-        if self.title.value.containsCaseInsentative(string: searchQuery) { return true }
+        if self.title.value.containsCaseInsensitive(string: searchQuery) { return true }
         return !self.participants.value.filter { participant in
-            participant.registeredName.value.containsCaseInsentative(string: searchQuery) ||
-                participant.profileName.value.containsCaseInsentative(string: searchQuery) || participant.jamiId.containsCaseInsentative(string: searchQuery)
+            participant.registeredName.value.containsCaseInsensitive(string: searchQuery) ||
+                participant.profileName.value.containsCaseInsensitive(string: searchQuery) || participant.jamiId.containsCaseInsensitive(string: searchQuery)
         }.isEmpty
     }