浏览代码

Merge pull request #1895 from nextcloud/feature/1772/reactions

Reactions to chat messages
Marcel Hibbe 3 年之前
父节点
当前提交
d66a6a9578
共有 65 个文件被更改,包括 1855 次插入296 次删除
  1. 2 12
      .idea/codeStyles/Project.xml
  2. 9 0
      CONTRIBUTING.md
  3. 8 0
      app/build.gradle
  4. 28 0
      app/src/main/java/com/nextcloud/talk/adapters/ReactionItem.kt
  5. 25 0
      app/src/main/java/com/nextcloud/talk/adapters/ReactionItemClickListener.kt
  6. 47 0
      app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt
  7. 88 0
      app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt
  8. 15 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt
  9. 4 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java
  10. 15 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt
  11. 15 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
  12. 15 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt
  13. 48 17
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  14. 15 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt
  15. 4 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java
  16. 15 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt
  17. 7 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt
  18. 93 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt
  19. 8 0
      app/src/main/java/com/nextcloud/talk/adapters/messages/ReactionsInterface.kt
  20. 19 3
      app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java
  21. 15 0
      app/src/main/java/com/nextcloud/talk/api/NcApi.java
  22. 82 8
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  23. 1 0
      app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt
  24. 49 31
      app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java
  25. 50 0
      app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReactionActorTypeConverter.kt
  26. 9 0
      app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt
  27. 47 0
      app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt
  28. 37 0
      app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOCS.kt
  29. 35 0
      app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOverall.kt
  30. 2 2
      app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt
  31. 1 1
      app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt
  32. 127 7
      app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt
  33. 351 0
      app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt
  34. 7 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  35. 1 1
      app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt
  36. 1 1
      app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
  37. 1 1
      app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java
  38. 10 4
      app/src/main/res/drawable/ic_delete.xml
  39. 26 0
      app/src/main/res/drawable/ic_dots_horizontal.xml
  40. 0 1
      app/src/main/res/layout/bottom_sheet.xml
  41. 0 1
      app/src/main/res/layout/controller_entry_menu.xml
  42. 5 6
      app/src/main/res/layout/dialog_attachment.xml
  43. 6 6
      app/src/main/res/layout/dialog_audio_output.xml
  44. 0 1
      app/src/main/res/layout/dialog_bottom_contacts.xml
  45. 12 13
      app/src/main/res/layout/dialog_conversation_operations.xml
  46. 252 164
      app/src/main/res/layout/dialog_message_actions.xml
  47. 48 0
      app/src/main/res/layout/dialog_message_reactions.xml
  48. 0 1
      app/src/main/res/layout/dialog_scope.xml
  49. 4 1
      app/src/main/res/layout/item_custom_incoming_location_message.xml
  50. 4 0
      app/src/main/res/layout/item_custom_incoming_preview_message.xml
  51. 5 2
      app/src/main/res/layout/item_custom_incoming_text_message.xml
  52. 4 0
      app/src/main/res/layout/item_custom_incoming_voice_message.xml
  53. 4 1
      app/src/main/res/layout/item_custom_outcoming_location_message.xml
  54. 4 0
      app/src/main/res/layout/item_custom_outcoming_preview_message.xml
  55. 4 0
      app/src/main/res/layout/item_custom_outcoming_text_message.xml
  56. 4 0
      app/src/main/res/layout/item_custom_outcoming_voice_message.xml
  57. 46 0
      app/src/main/res/layout/item_reactions_tab.xml
  58. 56 0
      app/src/main/res/layout/reaction_item.xml
  59. 36 0
      app/src/main/res/layout/reactions_inside_message.xml
  60. 4 2
      app/src/main/res/values-night/colors.xml
  61. 5 2
      app/src/main/res/values/colors.xml
  62. 8 0
      app/src/main/res/values/strings.xml
  63. 10 1
      app/src/main/res/values/styles.xml
  64. 1 1
      detekt.yml
  65. 1 1
      scripts/analysis/lint-results.txt

+ 2 - 12
.idea/codeStyles/Project.xml

@@ -56,16 +56,7 @@
     <JetCodeStyleSettings>
     <JetCodeStyleSettings>
       <option name="PACKAGES_TO_USE_STAR_IMPORTS">
       <option name="PACKAGES_TO_USE_STAR_IMPORTS">
         <value>
         <value>
-          <package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" static="false" />
-        </value>
-      </option>
-      <option name="PACKAGES_IMPORT_LAYOUT">
-        <value>
-          <package name="" alias="false" withSubpackages="true" />
-          <package name="java" alias="false" withSubpackages="true" />
-          <package name="javax" alias="false" withSubpackages="true" />
-          <package name="kotlin" alias="false" withSubpackages="true" />
-          <package name="" alias="true" withSubpackages="true" />
+          <package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
         </value>
         </value>
       </option>
       </option>
       <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
       <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
@@ -212,8 +203,7 @@
       <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
       <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
       <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
       <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
       <indentOptions>
       <indentOptions>
-		<option name="INDENT_SIZE" value="4" />
-		<option name="CONTINUATION_INDENT_SIZE" value="4" />
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
       </indentOptions>
       </indentOptions>
     </codeStyleSettings>
     </codeStyleSettings>
   </code_scheme>
   </code_scheme>

+ 9 - 0
CONTRIBUTING.md

@@ -141,6 +141,15 @@ If you set your `user.name` and `user.email` git configs, you can sign your comm
 You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases) like `git config --global alias.ci 'commit -s'`.
 You can also use git [aliases](https://git-scm.com/book/tr/v2/Git-Basics-Git-Aliases) like `git config --global alias.ci 'commit -s'`.
 Now you can commit with `git ci` and the commit will be signed.
 Now you can commit with `git ci` and the commit will be signed.
 
 
+### Git hooks
+
+We provide git hooks to make development process easier for both the developer and the reviewers.
+To install them, just run:
+
+```bash
+./gradlew installGitHooks
+```
+
 ## Contribution process
 ## Contribution process
 
 
 Contribute your code targeting/based-on the branch ```master```.
 Contribute your code targeting/based-on the branch ```master```.

+ 8 - 0
app/build.gradle

@@ -334,6 +334,14 @@ dependencies {
     gplayImplementation "com.google.firebase:firebase-messaging:23.0.0"
     gplayImplementation "com.google.firebase:firebase-messaging:23.0.0"
 }
 }
 
 
+task installGitHooks(type: Copy, group: "development") {
+    description = "Install git hooks"
+    from("../scripts/hooks") {
+        include '*'
+    }
+    into '../.git/hooks'
+}
+
 detekt {
 detekt {
     reports {
     reports {
         xml {
         xml {

+ 28 - 0
app/src/main/java/com/nextcloud/talk/adapters/ReactionItem.kt

@@ -0,0 +1,28 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters
+
+import com.nextcloud.talk.models.json.reactions.ReactionVoter
+
+data class ReactionItem(
+    val reactionVoter: ReactionVoter,
+    val reaction: String?
+)

+ 25 - 0
app/src/main/java/com/nextcloud/talk/adapters/ReactionItemClickListener.kt

@@ -0,0 +1,25 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters
+
+interface ReactionItemClickListener {
+    fun onClick(reactionItem: ReactionItem)
+}

+ 47 - 0
app/src/main/java/com/nextcloud/talk/adapters/ReactionsAdapter.kt

@@ -0,0 +1,47 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.databinding.ReactionItemBinding
+import com.nextcloud.talk.models.database.UserEntity
+
+class ReactionsAdapter(
+    private val clickListener: ReactionItemClickListener,
+    private val userEntity: UserEntity?
+) : RecyclerView.Adapter<ReactionsViewHolder>() {
+    internal var list: MutableList<ReactionItem> = ArrayList<ReactionItem>()
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReactionsViewHolder {
+        val itemBinding = ReactionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        return ReactionsViewHolder(itemBinding, userEntity?.baseUrl)
+    }
+
+    override fun onBindViewHolder(holder: ReactionsViewHolder, position: Int) {
+        holder.bind(list[position], clickListener)
+    }
+
+    override fun getItemCount(): Int {
+        return list.size
+    }
+}

+ 88 - 0
app/src/main/java/com/nextcloud/talk/adapters/ReactionsViewHolder.kt

@@ -0,0 +1,88 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters
+
+import android.text.TextUtils
+import androidx.recyclerview.widget.RecyclerView
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.interfaces.DraweeController
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.databinding.ReactionItemBinding
+import com.nextcloud.talk.models.json.reactions.ReactionVoter
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+
+class ReactionsViewHolder(
+    private val binding: ReactionItemBinding,
+    private val baseUrl: String?
+) : RecyclerView.ViewHolder(binding.root) {
+
+    fun bind(reactionItem: ReactionItem, clickListener: ReactionItemClickListener) {
+        binding.root.setOnClickListener { clickListener.onClick(reactionItem) }
+        binding.reaction.text = reactionItem.reaction
+        binding.name.text = reactionItem.reactionVoter.actorDisplayName
+
+        if (baseUrl != null && baseUrl.isNotEmpty()) {
+            loadAvatar(reactionItem)
+        }
+    }
+
+    private fun loadAvatar(reactionItem: ReactionItem) {
+        if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.GUESTS) {
+            var displayName = sharedApplication?.resources?.getString(R.string.nc_guest)
+            if (!TextUtils.isEmpty(reactionItem.reactionVoter.actorDisplayName)) {
+                displayName = reactionItem.reactionVoter.actorDisplayName!!
+            }
+            val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
+                .setOldController(binding.avatar.controller)
+                .setAutoPlayAnimations(true)
+                .setImageRequest(
+                    DisplayUtils.getImageRequestForUrl(
+                        ApiUtils.getUrlForGuestAvatar(
+                            baseUrl,
+                            displayName,
+                            false
+                        ),
+                        null
+                    )
+                )
+                .build()
+            binding.avatar.controller = draweeController
+        } else if (reactionItem.reactionVoter.actorType == ReactionVoter.ReactionActorType.USERS) {
+            val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
+                .setOldController(binding.avatar.controller)
+                .setAutoPlayAnimations(true)
+                .setImageRequest(
+                    DisplayUtils.getImageRequestForUrl(
+                        ApiUtils.getUrlForAvatar(
+                            baseUrl,
+                            reactionItem.reactionVoter.actorId,
+                            false
+                        ),
+                        null
+                    )
+                )
+                .build()
+            binding.avatar.controller = draweeController
+        }
+    }
+}

+ 15 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt

@@ -78,6 +78,8 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
     @Inject
     @Inject
     var appPreferences: AppPreferences? = null
     var appPreferences: AppPreferences? = null
 
 
+    lateinit var reactionsInterface: ReactionsInterface
+
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
         super.onBind(message)
         super.onBind(message)
@@ -93,13 +95,21 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
         val textSize = context?.resources!!.getDimension(R.dimen.chat_text_size)
         val textSize = context?.resources!!.getDimension(R.dimen.chat_text_size)
         binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
         binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
         binding.messageText.text = message.text
         binding.messageText.text = message.text
-        binding.messageText.isEnabled = false
 
 
         // parent message handling
         // parent message handling
         setParentMessageDataOnMessageItem(message)
         setParentMessageDataOnMessageItem(message)
 
 
         // geo-location
         // geo-location
         setLocationDataOnMessageItem(message)
         setLocationDataOnMessageItem(message)
+
+        Reaction().showReactions(message, binding.reactions, context!!, true)
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
     }
     }
 
 
     private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
     private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
@@ -270,6 +280,10 @@ class IncomingLocationMessageViewHolder(incomingView: View, payload: Any) : Mess
         return locationGeoLink.replace("geo:", "geo:0,0?q=")
         return locationGeoLink.replace("geo:", "geo:0,0?q=")
     }
     }
 
 
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
     companion object {
     companion object {
         private const val TAG = "LocInMessageView"
         private const val TAG = "LocInMessageView"
     }
     }

+ 4 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java

@@ -27,6 +27,7 @@ import android.widget.ProgressBar;
 
 
 import com.facebook.drawee.view.SimpleDraweeView;
 import com.facebook.drawee.view.SimpleDraweeView;
 import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
 import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding;
+import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
 
 
 import androidx.emoji.widget.EmojiTextView;
 import androidx.emoji.widget.EmojiTextView;
 
 
@@ -77,4 +78,7 @@ public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHol
     public ProgressBar getPreviewContactProgressBar() {
     public ProgressBar getPreviewContactProgressBar() {
         return binding.contactProgressBar;
         return binding.contactProgressBar;
     }
     }
+
+    @Override
+    public ReactionsInsideMessageBinding getReactionsBinding(){ return binding.reactions; }
 }
 }

+ 15 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -74,6 +74,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
     lateinit var message: ChatMessage
     lateinit var message: ChatMessage
 
 
     lateinit var voiceMessageInterface: VoiceMessageInterface
     lateinit var voiceMessageInterface: VoiceMessageInterface
+    lateinit var reactionsInterface: ReactionsInterface
 
 
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
@@ -140,6 +141,15 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
                 }
                 }
             }
             }
         })
         })
+
+        Reaction().showReactions(message, binding.reactions, context!!, true)
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
     }
     }
 
 
     private fun updateDownloadState(message: ChatMessage) {
     private fun updateDownloadState(message: ChatMessage) {
@@ -302,10 +312,14 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
         }
         }
     }
     }
 
 
-    fun assignAdapter(voiceMessageInterface: VoiceMessageInterface) {
+    fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) {
         this.voiceMessageInterface = voiceMessageInterface
         this.voiceMessageInterface = voiceMessageInterface
     }
     }
 
 
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
     companion object {
     companion object {
         private const val TAG = "VoiceInMessageView"
         private const val TAG = "VoiceInMessageView"
         private const val SEEKBAR_START: Int = 0
         private const val SEEKBAR_START: Int = 0

+ 15 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt

@@ -71,6 +71,8 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
     @Inject
     @Inject
     var appPreferences: AppPreferences? = null
     var appPreferences: AppPreferences? = null
 
 
+    lateinit var reactionsInterface: ReactionsInterface
+
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
         super.onBind(message)
         super.onBind(message)
         sharedApplication!!.componentApplication.inject(this)
         sharedApplication!!.componentApplication.inject(this)
@@ -119,6 +121,15 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
         }
         }
 
 
         itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
         itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
+
+        Reaction().showReactions(message, binding.reactions, context!!, true)
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
     }
     }
 
 
     private fun processAuthor(message: ChatMessage) {
     private fun processAuthor(message: ChatMessage) {
@@ -260,6 +271,10 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
         return messageStringInternal
         return messageStringInternal
     }
     }
 
 
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
     companion object {
     companion object {
         const val TEXT_SIZE_MULTIPLIER = 2.5
         const val TEXT_SIZE_MULTIPLIER = 2.5
     }
     }

+ 15 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt

@@ -61,6 +61,8 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
     @Inject
     @Inject
     var context: Context? = null
     var context: Context? = null
 
 
+    lateinit var reactionsInterface: ReactionsInterface
+
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
         super.onBind(message)
         super.onBind(message)
         sharedApplication!!.componentApplication.inject(this)
         sharedApplication!!.componentApplication.inject(this)
@@ -118,6 +120,15 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
         binding.checkMark.setContentDescription(readStatusContentDescriptionString)
         binding.checkMark.setContentDescription(readStatusContentDescriptionString)
 
 
         itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
         itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
+
+        Reaction().showReactions(message, binding.reactions, context!!, true)
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
     }
     }
 
 
     private fun processParentMessage(message: ChatMessage) {
     private fun processParentMessage(message: ChatMessage) {
@@ -204,6 +215,10 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
         return messageString1
         return messageString1
     }
     }
 
 
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
     companion object {
     companion object {
         const val TEXT_SIZE_MULTIPLIER = 2.5
         const val TEXT_SIZE_MULTIPLIER = 2.5
     }
     }

+ 48 - 17
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java

@@ -52,6 +52,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.components.filebrowser.models.BrowserFile;
 import com.nextcloud.talk.components.filebrowser.models.BrowserFile;
 import com.nextcloud.talk.components.filebrowser.models.DavResponse;
 import com.nextcloud.talk.components.filebrowser.models.DavResponse;
 import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
 import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
+import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
 import com.nextcloud.talk.jobs.DownloadFileToCacheWorker;
 import com.nextcloud.talk.jobs.DownloadFileToCacheWorker;
 import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.database.UserEntity;
@@ -111,8 +112,13 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
 
 
     ProgressBar progressBar;
     ProgressBar progressBar;
 
 
+    ReactionsInsideMessageBinding reactionsBinding;
+
     View clickView;
     View clickView;
 
 
+    ReactionsInterface reactionsInterface;
+    PreviewMessageInterface previewMessageInterface;
+
     public MagicPreviewMessageViewHolder(View itemView, Object payload) {
     public MagicPreviewMessageViewHolder(View itemView, Object payload) {
         super(itemView, payload);
         super(itemView, payload);
         NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
         NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
@@ -185,25 +191,30 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
                 fetchFileInformation("/" + message.getSelectedIndividualHashMap().get(KEY_PATH), message.activeUser);
                 fetchFileInformation("/" + message.getSelectedIndividualHashMap().get(KEY_PATH), message.activeUser);
             }
             }
 
 
-            String accountString =
+            if(message.activeUser != null && message.activeUser.getUsername() != null && message.activeUser.getBaseUrl() != null){
+                String accountString =
                     message.activeUser.getUsername() + "@" +
                     message.activeUser.getUsername() + "@" +
-                            message.activeUser.getBaseUrl()
-                                    .replace("https://", "")
-                                    .replace("http://", "");
+                        message.activeUser.getBaseUrl()
+                            .replace("https://", "")
+                            .replace("http://", "");
 
 
-            clickView.setOnClickListener(v -> {
-                String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
-                if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileName)) {
-                    openOrDownloadFile(message);
-                } else {
-                    openFileInFilesApp(message, accountString);
-                }
-            });
+                clickView.setOnClickListener(v -> {
+                    String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
+                    if (isSupportedForInternalViewer(mimetype) || canBeHandledByExternalApp(mimetype, fileName)) {
+                        openOrDownloadFile(message);
+                    } else {
+                        openFileInFilesApp(message, accountString);
+                    }
+                });
+
+                clickView.setOnLongClickListener(l -> {
+                    onMessageViewLongClick(message, accountString);
+                    return true;
+                });
+            } else {
+                Log.e(TAG, "failed to set click listener because activeUser, username or baseUrl were null");
+            }
 
 
-            clickView.setOnLongClickListener(l -> {
-                onMessageViewLongClick(message, accountString);
-                return true;
-            });
 
 
             // check if download worker is already running
             // check if download worker is already running
             String fileId = message.getSelectedIndividualHashMap().get(KEY_ID);
             String fileId = message.getSelectedIndividualHashMap().get(KEY_ID);
@@ -246,8 +257,17 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
         }
         }
 
 
         itemView.setTag(REPLYABLE_VIEW_TAG, message.isReplyable());
         itemView.setTag(REPLYABLE_VIEW_TAG, message.isReplyable());
-    }
 
 
+        reactionsBinding = getReactionsBinding();
+        new Reaction().showReactions(message, reactionsBinding, context, false);
+        reactionsBinding.reactionsEmojiWrapper.setOnClickListener(l -> {
+            reactionsInterface.onClickReactions(message);
+        });
+        reactionsBinding.reactionsEmojiWrapper.setOnLongClickListener(l -> {
+            reactionsInterface.onLongClickReactions(message);
+            return true;
+        });
+    }
 
 
     private Drawable getDrawableFromContactDetails(Context context, String base64) {
     private Drawable getDrawableFromContactDetails(Context context, String base64) {
         Drawable drawable = null;
         Drawable drawable = null;
@@ -283,6 +303,8 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
 
 
     public abstract ProgressBar getPreviewContactProgressBar();
     public abstract ProgressBar getPreviewContactProgressBar();
 
 
+    public abstract ReactionsInsideMessageBinding getReactionsBinding();
+
     private void openOrDownloadFile(ChatMessage message) {
     private void openOrDownloadFile(ChatMessage message) {
         String filename = message.getSelectedIndividualHashMap().get(KEY_NAME);
         String filename = message.getSelectedIndividualHashMap().get(KEY_NAME);
         String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
         String mimetype = message.getSelectedIndividualHashMap().get(KEY_MIMETYPE);
@@ -410,6 +432,7 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
 
 
     private void onMessageViewLongClick(ChatMessage message, String accountString) {
     private void onMessageViewLongClick(ChatMessage message, String accountString) {
         if (isSupportedForInternalViewer(message.getSelectedIndividualHashMap().get(KEY_MIMETYPE))) {
         if (isSupportedForInternalViewer(message.getSelectedIndividualHashMap().get(KEY_MIMETYPE))) {
+            previewMessageInterface.onPreviewMessageLongClick(message);
             return;
             return;
         }
         }
 
 
@@ -591,4 +614,12 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
                     }
                     }
                 });
                 });
     }
     }
+
+    public void assignReactionInterface(ReactionsInterface reactionsInterface) {
+        this.reactionsInterface = reactionsInterface;
+    }
+
+    public void assignPreviewMessageInterface(PreviewMessageInterface previewMessageInterface) {
+        this.previewMessageInterface = previewMessageInterface;
+    }
 }
 }

+ 15 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt

@@ -68,6 +68,8 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
     @Inject
     @Inject
     var context: Context? = null
     var context: Context? = null
 
 
+    lateinit var reactionsInterface: ReactionsInterface
+
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
         super.onBind(message)
         super.onBind(message)
@@ -84,7 +86,6 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
         binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
         binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
         binding.messageTime.layoutParams = layoutParams
         binding.messageTime.layoutParams = layoutParams
         binding.messageText.text = message.text
         binding.messageText.text = message.text
-        binding.messageText.isEnabled = false
 
 
         // parent message handling
         // parent message handling
         setParentMessageDataOnMessageItem(message)
         setParentMessageDataOnMessageItem(message)
@@ -112,6 +113,15 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
 
 
         // geo-location
         // geo-location
         setLocationDataOnMessageItem(message)
         setLocationDataOnMessageItem(message)
+
+        Reaction().showReactions(message, binding.reactions, context!!, true)
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
     }
     }
 
 
     @SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
     @SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
@@ -245,6 +255,10 @@ class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders
         return locationGeoLink.replace("geo:", "geo:0,0?q=")
         return locationGeoLink.replace("geo:", "geo:0,0?q=")
     }
     }
 
 
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
     companion object {
     companion object {
         private const val TAG = "LocOutMessageView"
         private const val TAG = "LocOutMessageView"
     }
     }

+ 4 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java

@@ -27,6 +27,7 @@ import android.widget.ProgressBar;
 
 
 import com.facebook.drawee.view.SimpleDraweeView;
 import com.facebook.drawee.view.SimpleDraweeView;
 import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
 import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding;
+import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
 
 
 import androidx.emoji.widget.EmojiTextView;
 import androidx.emoji.widget.EmojiTextView;
 
 
@@ -77,4 +78,7 @@ public class OutcomingPreviewMessageViewHolder extends MagicPreviewMessageViewHo
     public ProgressBar getPreviewContactProgressBar() {
     public ProgressBar getPreviewContactProgressBar() {
         return binding.contactProgressBar;
         return binding.contactProgressBar;
     }
     }
+
+    @Override
+    public ReactionsInsideMessageBinding getReactionsBinding() { return binding.reactions; }
 }
 }

+ 15 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt

@@ -69,6 +69,7 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
     lateinit var handler: Handler
     lateinit var handler: Handler
 
 
     lateinit var voiceMessageInterface: VoiceMessageInterface
     lateinit var voiceMessageInterface: VoiceMessageInterface
+    lateinit var reactionsInterface: ReactionsInterface
 
 
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
     override fun onBind(message: ChatMessage) {
@@ -129,6 +130,15 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
         }
         }
 
 
         binding.checkMark.setContentDescription(readStatusContentDescriptionString)
         binding.checkMark.setContentDescription(readStatusContentDescriptionString)
+
+        Reaction().showReactions(message, binding.reactions, context!!, true)
+        binding.reactions.reactionsEmojiWrapper.setOnClickListener {
+            reactionsInterface.onClickReactions(message)
+        }
+        binding.reactions.reactionsEmojiWrapper.setOnLongClickListener { l: View? ->
+            reactionsInterface.onLongClickReactions(message)
+            true
+        }
     }
     }
 
 
     private fun handleResetVoiceMessageState(message: ChatMessage) {
     private fun handleResetVoiceMessageState(message: ChatMessage) {
@@ -275,10 +285,14 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
         }
         }
     }
     }
 
 
-    fun assignAdapter(voiceMessageInterface: VoiceMessageInterface) {
+    fun assignVoiceMessageInterface(voiceMessageInterface: VoiceMessageInterface) {
         this.voiceMessageInterface = voiceMessageInterface
         this.voiceMessageInterface = voiceMessageInterface
     }
     }
 
 
+    fun assignReactionInterface(reactionsInterface: ReactionsInterface) {
+        this.reactionsInterface = reactionsInterface
+    }
+
     companion object {
     companion object {
         private const val TAG = "VoiceOutMessageView"
         private const val TAG = "VoiceOutMessageView"
         private const val SEEKBAR_START: Int = 0
         private const val SEEKBAR_START: Int = 0

+ 7 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageInterface.kt

@@ -0,0 +1,7 @@
+package com.nextcloud.talk.adapters.messages
+
+import com.nextcloud.talk.models.json.chat.ChatMessage
+
+interface PreviewMessageInterface {
+    fun onPreviewMessageLongClick(chatMessage: ChatMessage)
+}

+ 93 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/Reaction.kt

@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Parts related to account import were either copied from or inspired by the great work done by David Luhmer at:
+ * https://github.com/nextcloud/ownCloud-Account-Importer
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import android.content.Context
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import com.nextcloud.talk.R
+import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.utils.DisplayUtils
+import com.vanniktech.emoji.EmojiTextView
+
+class Reaction {
+    fun showReactions(
+        message: ChatMessage,
+        binding: ReactionsInsideMessageBinding,
+        context: Context,
+        useLightColorForText: Boolean
+    ) {
+        binding.reactionsEmojiWrapper.removeAllViews()
+        if (message.reactions != null && message.reactions.isNotEmpty()) {
+
+            var remainingEmojisToDisplay = MAX_EMOJIS_TO_DISPLAY
+            val showInfoAboutMoreEmojis = message.reactions.size > MAX_EMOJIS_TO_DISPLAY
+            for ((emoji, amount) in message.reactions) {
+                val reactionEmoji = EmojiTextView(context)
+                reactionEmoji.text = emoji
+                binding.reactionsEmojiWrapper.addView(reactionEmoji)
+
+                val reactionAmount = TextView(context)
+
+                if (amount > 1) {
+                    if (useLightColorForText) {
+                        reactionAmount.setTextColor(ContextCompat.getColor(context, R.color.nc_grey))
+                    }
+                    reactionAmount.text = amount.toString()
+                }
+
+                val params = RelativeLayout.LayoutParams(
+                    ViewGroup.LayoutParams.WRAP_CONTENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT
+                )
+                params.setMargins(
+                    DisplayUtils.convertDpToPixel(EMOJI_START_MARGIN, context).toInt(),
+                    0,
+                    DisplayUtils.convertDpToPixel(EMOJI_END_MARGIN, context).toInt(),
+                    0
+                )
+                reactionAmount.layoutParams = params
+                binding.reactionsEmojiWrapper.addView(reactionAmount)
+
+                remainingEmojisToDisplay--
+                if (remainingEmojisToDisplay == 0 && showInfoAboutMoreEmojis) {
+                    val infoAboutMoreEmojis = TextView(context)
+                    infoAboutMoreEmojis.text = EMOJI_MORE
+                    binding.reactionsEmojiWrapper.addView(infoAboutMoreEmojis)
+                    break
+                }
+            }
+        }
+    }
+
+    companion object {
+        const val MAX_EMOJIS_TO_DISPLAY = 4
+        const val EMOJI_START_MARGIN: Float = 2F
+        const val EMOJI_END_MARGIN: Float = 8F
+        const val EMOJI_MORE = "…"
+    }
+}

+ 8 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/ReactionsInterface.kt

@@ -0,0 +1,8 @@
+package com.nextcloud.talk.adapters.messages
+
+import com.nextcloud.talk.models.json.chat.ChatMessage
+
+interface ReactionsInterface {
+    fun onClickReactions(chatMessage: ChatMessage)
+    fun onLongClickReactions(chatMessage: ChatMessage)
+}

+ 19 - 3
app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java

@@ -49,10 +49,26 @@ public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAda
     public void onBindViewHolder(ViewHolder holder, int position) {
     public void onBindViewHolder(ViewHolder holder, int position) {
         super.onBindViewHolder(holder, position);
         super.onBindViewHolder(holder, position);
 
 
-        if (holder instanceof IncomingVoiceMessageViewHolder) {
-            ((IncomingVoiceMessageViewHolder) holder).assignAdapter(chatController);
+        if (holder instanceof MagicIncomingTextMessageViewHolder) {
+            ((MagicIncomingTextMessageViewHolder) holder).assignReactionInterface(chatController);
+        } else if (holder instanceof MagicOutcomingTextMessageViewHolder) {
+            ((MagicOutcomingTextMessageViewHolder) holder).assignReactionInterface(chatController);
+
+        } else if (holder instanceof IncomingLocationMessageViewHolder) {
+            ((IncomingLocationMessageViewHolder) holder).assignReactionInterface(chatController);
+        } else if (holder instanceof OutcomingLocationMessageViewHolder) {
+            ((OutcomingLocationMessageViewHolder) holder).assignReactionInterface(chatController);
+
+        } else if (holder instanceof IncomingVoiceMessageViewHolder) {
+            ((IncomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
+            ((IncomingVoiceMessageViewHolder) holder).assignReactionInterface(chatController);
         } else if (holder instanceof OutcomingVoiceMessageViewHolder) {
         } else if (holder instanceof OutcomingVoiceMessageViewHolder) {
-            ((OutcomingVoiceMessageViewHolder) holder).assignAdapter(chatController);
+            ((OutcomingVoiceMessageViewHolder) holder).assignVoiceMessageInterface(chatController);
+            ((OutcomingVoiceMessageViewHolder) holder).assignReactionInterface(chatController);
+
+        } else if (holder instanceof MagicPreviewMessageViewHolder) {
+            ((MagicPreviewMessageViewHolder) holder).assignPreviewMessageInterface(chatController);
+            ((MagicPreviewMessageViewHolder) holder).assignReactionInterface(chatController);
         }
         }
     }
     }
 }
 }

+ 15 - 0
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -37,6 +37,7 @@ import com.nextcloud.talk.models.json.notifications.NotificationOverall;
 import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
 import com.nextcloud.talk.models.json.participants.AddParticipantOverall;
 import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
 import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
 import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
 import com.nextcloud.talk.models.json.push.PushRegistrationOverall;
+import com.nextcloud.talk.models.json.reactions.ReactionsOverall;
 import com.nextcloud.talk.models.json.search.ContactsByNumberOverall;
 import com.nextcloud.talk.models.json.search.ContactsByNumberOverall;
 import com.nextcloud.talk.models.json.signaling.SignalingOverall;
 import com.nextcloud.talk.models.json.signaling.SignalingOverall;
 import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
 import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
@@ -62,6 +63,7 @@ import retrofit2.http.FieldMap;
 import retrofit2.http.FormUrlEncoded;
 import retrofit2.http.FormUrlEncoded;
 import retrofit2.http.GET;
 import retrofit2.http.GET;
 import retrofit2.http.Header;
 import retrofit2.http.Header;
+import retrofit2.http.Headers;
 import retrofit2.http.Multipart;
 import retrofit2.http.Multipart;
 import retrofit2.http.POST;
 import retrofit2.http.POST;
 import retrofit2.http.PUT;
 import retrofit2.http.PUT;
@@ -488,4 +490,17 @@ public interface NcApi {
     @GET
     @GET
     Observable<StatusesOverall> getUserStatuses(@Header("Authorization") String authorization, @Url String url);
     Observable<StatusesOverall> getUserStatuses(@Header("Authorization") String authorization, @Url String url);
 
 
+
+    @POST
+    Observable<GenericOverall> sendReaction(@Header("Authorization") String authorization, @Url String url,
+                                 @Query("reaction") String reaction);
+
+    @DELETE
+    Observable<GenericOverall> deleteReaction(@Header("Authorization") String authorization, @Url String url,
+                                              @Query("reaction") String reaction);
+
+    @GET
+    Observable<ReactionsOverall> getReactions(@Header("Authorization") String authorization,
+                                              @Url String url,
+                                              @Query("reaction") String reaction);
 }
 }

+ 82 - 8
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -110,6 +110,8 @@ import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
 import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
+import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
+import com.nextcloud.talk.adapters.messages.ReactionsInterface
 import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
 import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
 import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
 import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.api.NcApi
@@ -139,6 +141,7 @@ import com.nextcloud.talk.presenters.MentionAutocompletePresenter
 import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.ui.dialog.AttachmentDialog
 import com.nextcloud.talk.ui.dialog.AttachmentDialog
 import com.nextcloud.talk.ui.dialog.MessageActionsDialog
 import com.nextcloud.talk.ui.dialog.MessageActionsDialog
+import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.ApiUtils
@@ -203,7 +206,9 @@ class ChatController(args: Bundle) :
     MessagesListAdapter.Formatter<Date>,
     MessagesListAdapter.Formatter<Date>,
     MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
     MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
     ContentChecker<ChatMessage>,
     ContentChecker<ChatMessage>,
-    VoiceMessageInterface {
+    VoiceMessageInterface,
+    ReactionsInterface,
+    PreviewMessageInterface {
 
 
     private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
     private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
 
 
@@ -2087,7 +2092,7 @@ class ChatController(args: Bundle) :
         if (response.code() == HTTP_CODE_OK) {
         if (response.code() == HTTP_CODE_OK) {
 
 
             val chatOverall = response.body() as ChatOverall?
             val chatOverall = response.body() as ChatOverall?
-            val chatMessageList = setDeletionFlagsAndRemoveInfomessages(chatOverall?.ocs!!.data)
+            val chatMessageList = handleSystemMessages(chatOverall?.ocs!!.data)
 
 
             if (chatMessageList.isNotEmpty() &&
             if (chatMessageList.isNotEmpty() &&
                 ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType
                 ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType
@@ -2336,14 +2341,16 @@ class ChatController(args: Bundle) :
         }
         }
     }
     }
 
 
-    private fun setDeletionFlagsAndRemoveInfomessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
+    private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
         val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
         val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
         val chatMessageIterator = chatMessageMap.iterator()
         val chatMessageIterator = chatMessageMap.iterator()
         while (chatMessageIterator.hasNext()) {
         while (chatMessageIterator.hasNext()) {
             val currentMessage = chatMessageIterator.next()
             val currentMessage = chatMessageIterator.next()
+
+            // setDeletionFlagsAndRemoveInfomessages
             if (isInfoMessageAboutDeletion(currentMessage)) {
             if (isInfoMessageAboutDeletion(currentMessage)) {
                 if (!chatMessageMap.containsKey(currentMessage.value.parentMessage.id)) {
                 if (!chatMessageMap.containsKey(currentMessage.value.parentMessage.id)) {
-                    // if chatMessageMap doesnt't contain message to delete (this happens when lookingIntoFuture),
+                    // if chatMessageMap doesn't contain message to delete (this happens when lookingIntoFuture),
                     // the message to delete has to be modified directly inside the adapter
                     // the message to delete has to be modified directly inside the adapter
                     setMessageAsDeleted(currentMessage.value.parentMessage)
                     setMessageAsDeleted(currentMessage.value.parentMessage)
                 } else {
                 } else {
@@ -2351,6 +2358,15 @@ class ChatController(args: Bundle) :
                 }
                 }
                 chatMessageIterator.remove()
                 chatMessageIterator.remove()
             }
             }
+
+            // delete reactions system messages
+            else if (isReactionsMessage(currentMessage)) {
+                if (!chatMessageMap.containsKey(currentMessage.value.parentMessage.id)) {
+                    updateAdapterForReaction(currentMessage.value.parentMessage)
+                }
+
+                chatMessageIterator.remove()
+            }
         }
         }
         return chatMessageMap.values.toList()
         return chatMessageMap.values.toList()
     }
     }
@@ -2360,6 +2376,12 @@ class ChatController(args: Bundle) :
             .SystemMessageType.MESSAGE_DELETED
             .SystemMessageType.MESSAGE_DELETED
     }
     }
 
 
+    private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
+        return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION ||
+            currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
+            currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
+    }
+
     private fun startACall(isVoiceOnlyCall: Boolean) {
     private fun startACall(isVoiceOnlyCall: Boolean) {
         if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) {
         if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) {
             Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
             Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
@@ -2398,21 +2420,50 @@ class ChatController(args: Bundle) :
         }
         }
     }
     }
 
 
+    override fun onClickReactions(chatMessage: ChatMessage) {
+        activity?.let {
+            ShowReactionsDialog(
+                activity!!,
+                currentConversation,
+                chatMessage,
+                conversationUser,
+                ncApi!!
+            ).show()
+        }
+    }
+
+    override fun onLongClickReactions(chatMessage: ChatMessage) {
+        openMessageActionsDialog(chatMessage)
+    }
+
     override fun onMessageViewLongClick(view: View?, message: IMessage?) {
     override fun onMessageViewLongClick(view: View?, message: IMessage?) {
-        if (hasVisibleItems(message as ChatMessage)) {
+        openMessageActionsDialog(message)
+    }
+
+    override fun onPreviewMessageLongClick(chatMessage: ChatMessage) {
+        openMessageActionsDialog(chatMessage)
+    }
+
+    private fun openMessageActionsDialog(iMessage: IMessage?) {
+        val message = iMessage as ChatMessage
+        if (hasVisibleItems(message) && !isSystemMessage(message)) {
             activity?.let {
             activity?.let {
                 MessageActionsDialog(
                 MessageActionsDialog(
-                    activity!!,
                     this,
                     this,
                     message,
                     message,
-                    conversationUser?.userId,
+                    conversationUser,
                     currentConversation,
                     currentConversation,
-                    isShowMessageDeletionButton(message)
+                    isShowMessageDeletionButton(message),
+                    ncApi!!
                 ).show()
                 ).show()
             }
             }
         }
         }
     }
     }
 
 
+    private fun isSystemMessage(message: ChatMessage): Boolean {
+        return ChatMessage.MessageType.SYSTEM_MESSAGE == message.getMessageType()
+    }
+
     fun deleteMessage(message: IMessage?) {
     fun deleteMessage(message: IMessage?) {
         var apiVersion = 1
         var apiVersion = 1
         // FIXME Fix API checking with guests?
         // FIXME Fix API checking with guests?
@@ -2680,6 +2731,29 @@ class ChatController(args: Bundle) :
         adapter?.update(messageTemp)
         adapter?.update(messageTemp)
     }
     }
 
 
+    private fun updateAdapterForReaction(message: IMessage?) {
+        val messageTemp = message as ChatMessage
+
+        messageTemp.isOneToOneConversation =
+            currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
+        messageTemp.activeUser = conversationUser
+
+        adapter?.update(messageTemp)
+    }
+
+    fun updateAdapterAfterSendReaction(message: ChatMessage, emoji: String) {
+        if (message.reactions == null) {
+            message.reactions = LinkedHashMap()
+        }
+
+        var amount = message.reactions[emoji]
+        if (amount == null) {
+            amount = 0
+        }
+        message.reactions[emoji] = amount + 1
+        adapter?.update(message)
+    }
+
     private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
     private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
         if (conversationUser == null) return false
         if (conversationUser == null) return false
 
 

+ 1 - 0
app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.kt

@@ -277,6 +277,7 @@ class WebViewLoginController(args: Bundle? = null) : NewBaseController(
                 }
                 }
             }
             }
 
 
+            @Suppress("Detekt.TooGenericExceptionCaught")
             override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
             override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
                 try {
                 try {
                     val sslCertificate = error.certificate
                     val sslCertificate = error.certificate

+ 49 - 31
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java

@@ -22,6 +22,7 @@
 package com.nextcloud.talk.models.json.chat;
 package com.nextcloud.talk.models.json.chat;
 
 
 import android.text.TextUtils;
 import android.text.TextUtils;
+import android.util.Log;
 
 
 import com.bluelinelabs.logansquare.annotation.JsonField;
 import com.bluelinelabs.logansquare.annotation.JsonField;
 import com.bluelinelabs.logansquare.annotation.JsonIgnore;
 import com.bluelinelabs.logansquare.annotation.JsonIgnore;
@@ -40,6 +41,7 @@ import java.security.MessageDigest;
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
@@ -50,6 +52,8 @@ import kotlin.text.Charsets;
 @Parcel
 @Parcel
 @JsonObject
 @JsonObject
 public class ChatMessage implements MessageContentType, MessageContentType.Image {
 public class ChatMessage implements MessageContentType, MessageContentType.Image {
+    private static String TAG = "ChatMessage";
+
     @JsonIgnore
     @JsonIgnore
     public boolean isGrouped;
     public boolean isGrouped;
     @JsonIgnore
     @JsonIgnore
@@ -90,6 +94,8 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
     public Enum<ReadStatus> readStatus = ReadStatus.NONE;
     public Enum<ReadStatus> readStatus = ReadStatus.NONE;
     @JsonField(name = "messageType")
     @JsonField(name = "messageType")
     public String messageType;
     public String messageType;
+    @JsonField(name = "reactions")
+    public LinkedHashMap<String, Integer> reactions;
 
 
     public boolean isDownloadingVoiceMessage;
     public boolean isDownloadingVoiceMessage;
     public boolean resetVoiceMessage;
     public boolean resetVoiceMessage;
@@ -100,21 +106,21 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
 
 
     @JsonIgnore
     @JsonIgnore
     List<MessageType> messageTypesToIgnore = Arrays.asList(
     List<MessageType> messageTypesToIgnore = Arrays.asList(
-            MessageType.REGULAR_TEXT_MESSAGE,
-            MessageType.SYSTEM_MESSAGE,
-            MessageType.SINGLE_LINK_VIDEO_MESSAGE,
-            MessageType.SINGLE_LINK_AUDIO_MESSAGE,
-            MessageType.SINGLE_LINK_MESSAGE,
-            MessageType.SINGLE_NC_GEOLOCATION_MESSAGE,
-            MessageType.VOICE_MESSAGE);
+        MessageType.REGULAR_TEXT_MESSAGE,
+        MessageType.SYSTEM_MESSAGE,
+        MessageType.SINGLE_LINK_VIDEO_MESSAGE,
+        MessageType.SINGLE_LINK_AUDIO_MESSAGE,
+        MessageType.SINGLE_LINK_MESSAGE,
+        MessageType.SINGLE_NC_GEOLOCATION_MESSAGE,
+        MessageType.VOICE_MESSAGE);
 
 
     public boolean hasFileAttachment() {
     public boolean hasFileAttachment() {
         if (messageParameters != null && messageParameters.size() > 0) {
         if (messageParameters != null && messageParameters.size() > 0) {
             for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
             for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
                 Map<String, String> individualHashMap = entry.getValue();
                 Map<String, String> individualHashMap = entry.getValue();
-                if(MessageDigest.isEqual(
-                        Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
-                        ("file").getBytes(Charsets.UTF_8))) {
+                if (MessageDigest.isEqual(
+                    Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
+                    ("file").getBytes(Charsets.UTF_8))) {
                     return true;
                     return true;
                 }
                 }
             }
             }
@@ -127,9 +133,9 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
             for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
             for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
                 Map<String, String> individualHashMap = entry.getValue();
                 Map<String, String> individualHashMap = entry.getValue();
 
 
-                if(MessageDigest.isEqual(
-                        Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
-                        ("geo-location").getBytes(Charsets.UTF_8))) {
+                if (MessageDigest.isEqual(
+                    Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
+                    ("geo-location").getBytes(Charsets.UTF_8))) {
                     return true;
                     return true;
                 }
                 }
             }
             }
@@ -144,13 +150,20 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
         if (messageParameters != null && messageParameters.size() > 0) {
         if (messageParameters != null && messageParameters.size() > 0) {
             for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
             for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
                 Map<String, String> individualHashMap = entry.getValue();
                 Map<String, String> individualHashMap = entry.getValue();
-                if(MessageDigest.isEqual(
-                        Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
-                        ("file").getBytes(Charsets.UTF_8))) {
+                if (MessageDigest.isEqual(
+                    Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8),
+                    ("file").getBytes(Charsets.UTF_8))) {
                     selectedIndividualHashMap = individualHashMap;
                     selectedIndividualHashMap = individualHashMap;
-                    if(!isVoiceMessage()){
-                        return (ApiUtils.getUrlForFilePreviewWithFileId(getActiveUser().getBaseUrl(),
-                                                                        individualHashMap.get("id"), NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size)));
+                    if (!isVoiceMessage()) {
+                        if (getActiveUser() != null && getActiveUser().getBaseUrl() != null) {
+                            return (ApiUtils.getUrlForFilePreviewWithFileId(
+                                getActiveUser().getBaseUrl(),
+                                individualHashMap.get("id"),
+                                NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size)));
+                        } else {
+                            Log.e(TAG, "getActiveUser() or getActiveUser().getBaseUrl() were null when trying to " +
+                                "getImageUrl()");
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -168,7 +181,7 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
             return MessageType.SYSTEM_MESSAGE;
             return MessageType.SYSTEM_MESSAGE;
         }
         }
 
 
-        if (isVoiceMessage()){
+        if (isVoiceMessage()) {
             return MessageType.VOICE_MESSAGE;
             return MessageType.VOICE_MESSAGE;
         }
         }
 
 
@@ -207,20 +220,20 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
             return getText();
             return getText();
         } else {
         } else {
             if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == getMessageType()
             if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == getMessageType()
-                    || MessageType.SINGLE_LINK_TENOR_MESSAGE == getMessageType()
-                    || MessageType.SINGLE_LINK_GIF_MESSAGE == getMessageType()) {
+                || MessageType.SINGLE_LINK_TENOR_MESSAGE == getMessageType()
+                || MessageType.SINGLE_LINK_GIF_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_gif_you));
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_gif_you));
                 } else {
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_gif),
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_gif),
-                            !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
+                                          !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
                 }
             } else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == getMessageType()) {
             } else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_attachment_you));
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_attachment_you));
                 } else {
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_attachment),
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_attachment),
-                            !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
+                                          !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
                 }
             } else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == getMessageType()) {
             } else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
@@ -248,21 +261,21 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_audio_you));
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_audio_you));
                 } else {
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_audio),
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_audio),
-                            !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
+                                          !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
                 }
             } else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == getMessageType()) {
             } else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_video_you));
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_video_you));
                 } else {
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_video),
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_video),
-                            !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
+                                          !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
                 }
             } else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == getMessageType()) {
             } else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == getMessageType()) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                 if (getActorId().equals(getActiveUser().getUserId())) {
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_image_you));
                     return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_image_you));
                 } else {
                 } else {
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_image),
                     return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_image),
-                            !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
+                                          !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest)));
                 }
                 }
             }
             }
         }
         }
@@ -289,14 +302,16 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
 
 
             @Override
             @Override
             public String getAvatar() {
             public String getAvatar() {
-                if (getActorType().equals("users")) {
+                if (getActiveUser() == null) {
+                    return null;
+                } else if (getActorType().equals("users")) {
                     return ApiUtils.getUrlForAvatar(getActiveUser().getBaseUrl(), actorId, true);
                     return ApiUtils.getUrlForAvatar(getActiveUser().getBaseUrl(), actorId, true);
                 } else if (getActorType().equals("bridged")) {
                 } else if (getActorType().equals("bridged")) {
                     return ApiUtils.getUrlForAvatar(getActiveUser().getBaseUrl(), "bridge-bot",
                     return ApiUtils.getUrlForAvatar(getActiveUser().getBaseUrl(), "bridge-bot",
                                                     true);
                                                     true);
                 } else {
                 } else {
                     String apiId =
                     String apiId =
-                            NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest);
+                        NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest);
 
 
                     if (!TextUtils.isEmpty(getActorDisplayName())) {
                     if (!TextUtils.isEmpty(getActorDisplayName())) {
                         apiId = getActorDisplayName();
                         apiId = getActorDisplayName();
@@ -592,7 +607,7 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
         return "ChatMessage(isGrouped=" + this.isGrouped() + ", isOneToOneConversation=" + this.isOneToOneConversation() + ", activeUser=" + this.getActiveUser() + ", selectedIndividualHashMap=" + this.getSelectedIndividualHashMap() + ", isDeleted=" + this.isDeleted() + ", jsonMessageId=" + this.getJsonMessageId() + ", token=" + this.getToken() + ", actorType=" + this.getActorType() + ", actorId=" + this.getActorId() + ", actorDisplayName=" + this.getActorDisplayName() + ", timestamp=" + this.getTimestamp() + ", message=" + this.getMessage() + ", messageParameters=" + this.getMessageParameters() + ", systemMessageType=" + this.getSystemMessageType() + ", replyable=" + this.isReplyable() + ", parentMessage=" + this.getParentMessage() + ", readStatus=" + this.getReadStatus() + ", messageTypesToIgnore=" + this.getMessageTypesToIgnore() + ")";
         return "ChatMessage(isGrouped=" + this.isGrouped() + ", isOneToOneConversation=" + this.isOneToOneConversation() + ", activeUser=" + this.getActiveUser() + ", selectedIndividualHashMap=" + this.getSelectedIndividualHashMap() + ", isDeleted=" + this.isDeleted() + ", jsonMessageId=" + this.getJsonMessageId() + ", token=" + this.getToken() + ", actorType=" + this.getActorType() + ", actorId=" + this.getActorId() + ", actorDisplayName=" + this.getActorDisplayName() + ", timestamp=" + this.getTimestamp() + ", message=" + this.getMessage() + ", messageParameters=" + this.getMessageParameters() + ", systemMessageType=" + this.getSystemMessageType() + ", replyable=" + this.isReplyable() + ", parentMessage=" + this.getParentMessage() + ", readStatus=" + this.getReadStatus() + ", messageTypesToIgnore=" + this.getMessageTypesToIgnore() + ")";
     }
     }
 
 
-    public boolean isVoiceMessage(){
+    public boolean isVoiceMessage() {
         return "voice-message".equals(messageType);
         return "voice-message".equals(messageType);
     }
     }
 
 
@@ -657,6 +672,9 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
         MATTERBRIDGE_CONFIG_REMOVED,
         MATTERBRIDGE_CONFIG_REMOVED,
         MATTERBRIDGE_CONFIG_ENABLED,
         MATTERBRIDGE_CONFIG_ENABLED,
         MATTERBRIDGE_CONFIG_DISABLED,
         MATTERBRIDGE_CONFIG_DISABLED,
-        CLEARED_CHAT
+        CLEARED_CHAT,
+        REACTION,
+        REACTION_DELETED,
+        REACTION_REVOKED
     }
     }
 }
 }

+ 50 - 0
app/src/main/java/com/nextcloud/talk/models/json/converters/EnumReactionActorTypeConverter.kt

@@ -0,0 +1,50 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.models.json.converters
+
+import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter
+import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.DUMMY
+import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.GUESTS
+import com.nextcloud.talk.models.json.reactions.ReactionVoter.ReactionActorType.USERS
+import com.nextcloud.talk.models.json.reactions.ReactionVoter
+
+class EnumReactionActorTypeConverter : StringBasedTypeConverter<ReactionVoter.ReactionActorType>() {
+    override fun getFromString(string: String): ReactionVoter.ReactionActorType {
+        return when (string) {
+            "guests" -> GUESTS
+            "users" -> USERS
+            else -> DUMMY
+        }
+    }
+
+    override fun convertToString(`object`: ReactionVoter.ReactionActorType?): String {
+
+        if (`object` == null) {
+            return ""
+        }
+
+        return when (`object`) {
+            GUESTS -> "guests"
+            USERS -> "users"
+            else -> ""
+        }
+    }
+}

+ 9 - 0
app/src/main/java/com/nextcloud/talk/models/json/converters/EnumSystemMessageTypeConverter.kt

@@ -65,6 +65,9 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERAT
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.OBJECT_SHARED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.OBJECT_SHARED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_SET
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_SET
+import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION
+import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_DELETED
+import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.REACTION_REVOKED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY_OFF
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.READ_ONLY_OFF
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_ADDED
 import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_ADDED
@@ -161,6 +164,9 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
             "matterbridge_config_enabled" -> return MATTERBRIDGE_CONFIG_ENABLED
             "matterbridge_config_enabled" -> return MATTERBRIDGE_CONFIG_ENABLED
             "matterbridge_config_disabled" -> return MATTERBRIDGE_CONFIG_DISABLED
             "matterbridge_config_disabled" -> return MATTERBRIDGE_CONFIG_DISABLED
             "history_cleared" -> return CLEARED_CHAT
             "history_cleared" -> return CLEARED_CHAT
+            "reaction" -> return REACTION
+            "reaction_deleted" -> return REACTION_DELETED
+            "reaction_revoked" -> return REACTION_REVOKED
             else -> return DUMMY
             else -> return DUMMY
         }
         }
     }
     }
@@ -214,6 +220,9 @@ class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.Syst
             MATTERBRIDGE_CONFIG_ENABLED -> return "matterbridge_config_enabled"
             MATTERBRIDGE_CONFIG_ENABLED -> return "matterbridge_config_enabled"
             MATTERBRIDGE_CONFIG_DISABLED -> return "matterbridge_config_disabled"
             MATTERBRIDGE_CONFIG_DISABLED -> return "matterbridge_config_disabled"
             CLEARED_CHAT -> return "clear_history"
             CLEARED_CHAT -> return "clear_history"
+            REACTION -> return "reaction"
+            REACTION_DELETED -> return "reaction_deleted"
+            REACTION_REVOKED -> return "reaction_revoked"
             else -> return ""
             else -> return ""
         }
         }
     }
     }

+ 47 - 0
app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionVoter.kt

@@ -0,0 +1,47 @@
+/*
+ *
+ *   Nextcloud Talk application
+ *
+ *   @author Marcel Hibbe
+ *   Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.reactions
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import com.nextcloud.talk.models.json.converters.EnumReactionActorTypeConverter
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class ReactionVoter(
+    @JsonField(name = ["actorType"], typeConverter = EnumReactionActorTypeConverter::class)
+    var actorType: ReactionActorType?,
+    @JsonField(name = ["actorId"])
+    var actorId: String?,
+    @JsonField(name = ["actorDisplayName"])
+    var actorDisplayName: String?,
+    @JsonField(name = ["timestamp"])
+    var timestamp: Long = 0
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null, null, 0)
+
+    enum class ReactionActorType {
+        DUMMY, GUESTS, USERS
+    }
+}

+ 37 - 0
app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOCS.kt

@@ -0,0 +1,37 @@
+/*
+ *   Nextcloud Talk application
+ *
+ *   @author Marcel Hibbe
+ *   Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.reactions
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import com.nextcloud.talk.models.json.generic.GenericOCS
+import kotlinx.android.parcel.Parcelize
+import java.util.HashMap
+
+@Parcelize
+@JsonObject
+data class ReactionsOCS(
+    @JsonField(name = ["data"])
+    var data: HashMap<String, List<ReactionVoter>>?
+) : GenericOCS(), Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(HashMap())
+}

+ 35 - 0
app/src/main/java/com/nextcloud/talk/models/json/reactions/ReactionsOverall.kt

@@ -0,0 +1,35 @@
+/*
+ *   Nextcloud Talk application
+ *
+ *   @author Marcel Hibbe
+ *   Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.json.reactions
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class ReactionsOverall(
+    @JsonField(name = ["ocs"])
+    var ocs: ReactionsOCS?
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 2 - 2
app/src/main/java/com/nextcloud/talk/models/json/status/Status.kt

@@ -32,7 +32,7 @@ data class Status(
     var userId: String?,
     var userId: String?,
     @JsonField(name = ["message"])
     @JsonField(name = ["message"])
     var message: String?,
     var message: String?,
-    /* TODO: Change to enum */
+    /* TODO Change to enum */
     @JsonField(name = ["messageId"])
     @JsonField(name = ["messageId"])
     var messageId: String?,
     var messageId: String?,
     @JsonField(name = ["messageIsPredefined"])
     @JsonField(name = ["messageIsPredefined"])
@@ -41,7 +41,7 @@ data class Status(
     var icon: String?,
     var icon: String?,
     @JsonField(name = ["clearAt"])
     @JsonField(name = ["clearAt"])
     var clearAt: Long = 0,
     var clearAt: Long = 0,
-    /* TODO: Change to enum */
+    /* TODO Change to enum */
     @JsonField(name = ["status"])
     @JsonField(name = ["status"])
     var status: String = "offline",
     var status: String = "offline",
     @JsonField(name = ["statusIsUserDefined"])
     @JsonField(name = ["statusIsUserDefined"])

+ 1 - 1
app/src/main/java/com/nextcloud/talk/ui/dialog/ConversationsListBottomDialog.kt

@@ -293,7 +293,7 @@ class ConversationsListBottomDialog(
 
 
         dialogRouter!!.pushController(
         dialogRouter!!.pushController(
 
 
-            // TODO: refresh conversation list after EntryMenuController finished (throw event? / pass controller
+            // TODO refresh conversation list after EntryMenuController finished (throw event? / pass controller
             //  into EntryMenuController to execute fetch data... ?!)
             //  into EntryMenuController to execute fetch data... ?!)
             // for example if you set a password, the dialog items should be refreshed for the next time you open it
             // for example if you set a password, the dialog items should be refreshed for the next time you open it
             // without to manually have to refresh the conversations list
             // without to manually have to refresh the conversations list

+ 127 - 7
app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt

@@ -20,42 +20,60 @@
 
 
 package com.nextcloud.talk.ui.dialog
 package com.nextcloud.talk.ui.dialog
 
 
-import android.app.Activity
+import android.annotation.SuppressLint
+import android.content.Context
 import android.os.Bundle
 import android.os.Bundle
+import android.util.Log
+import android.view.MotionEvent
 import android.view.View
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.NonNull
 import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.bottomsheet.BottomSheetDialog
 import com.google.android.material.bottomsheet.BottomSheetDialog
 import com.nextcloud.talk.BuildConfig
 import com.nextcloud.talk.BuildConfig
 import com.nextcloud.talk.R
 import com.nextcloud.talk.R
+import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.controllers.ChatController
 import com.nextcloud.talk.controllers.ChatController
 import com.nextcloud.talk.databinding.DialogMessageActionsBinding
 import com.nextcloud.talk.databinding.DialogMessageActionsBinding
+import com.nextcloud.talk.models.database.CapabilitiesUtil
+import com.nextcloud.talk.models.database.UserEntity
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.chat.ChatMessage
 import com.nextcloud.talk.models.json.conversations.Conversation
 import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.utils.ApiUtils
+import com.vanniktech.emoji.EmojiPopup
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
 
 
 class MessageActionsDialog(
 class MessageActionsDialog(
-    val activity: Activity,
     private val chatController: ChatController,
     private val chatController: ChatController,
     private val message: ChatMessage,
     private val message: ChatMessage,
-    private val userId: String?,
+    private val user: UserEntity?,
     private val currentConversation: Conversation?,
     private val currentConversation: Conversation?,
-    private val showMessageDeletionButton: Boolean
-) : BottomSheetDialog(activity) {
+    private val showMessageDeletionButton: Boolean,
+    private val ncApi: NcApi
+) : BottomSheetDialog(chatController.activity!!, R.style.BottomSheetDialogThemeNoFloating) {
 
 
     private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding
     private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding
 
 
+    private lateinit var popup: EmojiPopup
+
     override fun onCreate(savedInstanceState: Bundle?) {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         super.onCreate(savedInstanceState)
         dialogMessageActionsBinding = DialogMessageActionsBinding.inflate(layoutInflater)
         dialogMessageActionsBinding = DialogMessageActionsBinding.inflate(layoutInflater)
         setContentView(dialogMessageActionsBinding.root)
         setContentView(dialogMessageActionsBinding.root)
         window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
         window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
 
 
+        initEmojiBar()
         initMenuItemCopy(!message.isDeleted)
         initMenuItemCopy(!message.isDeleted)
         initMenuReplyToMessage(message.replyable)
         initMenuReplyToMessage(message.replyable)
         initMenuReplyPrivately(
         initMenuReplyPrivately(
             message.replyable &&
             message.replyable &&
-                userId?.isNotEmpty() == true &&
-                userId != "?" &&
+                user?.userId?.isNotEmpty() == true &&
+                user?.userId != "?" &&
                 message.user.id.startsWith("users/") &&
                 message.user.id.startsWith("users/") &&
                 message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
                 message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
                 currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
                 currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
@@ -67,6 +85,69 @@ class MessageActionsDialog(
                 ChatMessage.MessageType.SYSTEM_MESSAGE != message.getMessageType() &&
                 ChatMessage.MessageType.SYSTEM_MESSAGE != message.getMessageType() &&
                 BuildConfig.DEBUG
                 BuildConfig.DEBUG
         )
         )
+
+        initEmojiMore()
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    private fun initEmojiMore() {
+        dialogMessageActionsBinding.emojiMore.setOnTouchListener { v, event ->
+            if (event.action == MotionEvent.ACTION_DOWN) {
+                popup.toggle()
+            }
+            true
+        }
+
+        popup = EmojiPopup.Builder
+            .fromRootView(dialogMessageActionsBinding.root)
+            .setOnEmojiPopupShownListener {
+                dialogMessageActionsBinding.emojiMore.clearFocus()
+                dialogMessageActionsBinding.messageActions.visibility = View.GONE
+            }
+            .setOnEmojiClickListener { _, imageView ->
+                popup.dismiss()
+                sendReaction(message, imageView.unicode)
+            }
+            .setOnEmojiPopupDismissListener {
+                dialogMessageActionsBinding.emojiMore.clearFocus()
+                dialogMessageActionsBinding.messageActions.visibility = View.VISIBLE
+
+                val imm: InputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as
+                    InputMethodManager
+                imm.hideSoftInputFromWindow(dialogMessageActionsBinding.emojiMore.windowToken, 0)
+            }
+            .build(dialogMessageActionsBinding.emojiMore)
+        dialogMessageActionsBinding.emojiMore.disableKeyboardInput(popup)
+        dialogMessageActionsBinding.emojiMore.forceSingleEmoji()
+    }
+
+    private fun initEmojiBar() {
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "reactions")) {
+            dialogMessageActionsBinding.emojiThumbsUp.setOnClickListener {
+                sendReaction(message, dialogMessageActionsBinding.emojiThumbsUp.text.toString())
+            }
+            dialogMessageActionsBinding.emojiThumbsDown.setOnClickListener {
+                sendReaction(message, dialogMessageActionsBinding.emojiThumbsDown.text.toString())
+            }
+            dialogMessageActionsBinding.emojiLaugh.setOnClickListener {
+                sendReaction(message, dialogMessageActionsBinding.emojiLaugh.text.toString())
+            }
+            dialogMessageActionsBinding.emojiHeart.setOnClickListener {
+                sendReaction(message, dialogMessageActionsBinding.emojiHeart.text.toString())
+            }
+            dialogMessageActionsBinding.emojiConfused.setOnClickListener {
+                sendReaction(message, dialogMessageActionsBinding.emojiConfused.text.toString())
+            }
+            dialogMessageActionsBinding.emojiSad.setOnClickListener {
+                sendReaction(message, dialogMessageActionsBinding.emojiSad.text.toString())
+            }
+            dialogMessageActionsBinding.emojiMore.setOnClickListener {
+                dismiss()
+            }
+            dialogMessageActionsBinding.emojiBar.visibility = View.VISIBLE
+        } else {
+            dialogMessageActionsBinding.emojiBar.visibility = View.GONE
+        }
     }
     }
 
 
     private fun initMenuMarkAsUnread(visible: Boolean) {
     private fun initMenuMarkAsUnread(visible: Boolean) {
@@ -150,8 +231,47 @@ class MessageActionsDialog(
         }
         }
     }
     }
 
 
+    private fun sendReaction(message: ChatMessage, emoji: String) {
+        val credentials = ApiUtils.getCredentials(user?.username, user?.token)
+
+        ncApi.sendReaction(
+            credentials,
+            ApiUtils.getUrlForMessageReaction(
+                user?.baseUrl,
+                currentConversation!!.token,
+                message.id
+            ),
+            emoji
+        )
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(@NonNull genericOverall: GenericOverall) {
+                    val statusCode = genericOverall.ocs.meta.statusCode
+                    if (statusCode == HTTP_CREATED) {
+                        chatController.updateAdapterAfterSendReaction(message, emoji)
+                    }
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "error while sending reaction")
+                }
+
+                override fun onComplete() {
+                    dismiss()
+                }
+            })
+    }
+
     companion object {
     companion object {
+        private const val TAG = "MessageActionsDialog"
         private const val ACTOR_LENGTH = 6
         private const val ACTOR_LENGTH = 6
         private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
         private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
+        private const val HTTP_OK: Int = 200
+        private const val HTTP_CREATED: Int = 201
     }
     }
 }
 }

+ 351 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/ShowReactionsDialog.kt

@@ -0,0 +1,351 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
+ * Copyright (C) 2022 Marcel Hibbe (dev@mhibbe.de)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Parts related to account import were either copied from or inspired by the great work done by David Luhmer at:
+ * https://github.com/nextcloud/ownCloud-Account-Importer
+ */
+package com.nextcloud.talk.ui.dialog
+
+import android.app.Activity
+import android.os.Bundle
+import android.util.Log
+import android.view.ViewGroup
+import androidx.annotation.NonNull
+import androidx.recyclerview.widget.LinearLayoutManager
+import autodagger.AutoInjector
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
+import com.nextcloud.talk.R
+import com.nextcloud.talk.adapters.ReactionItem
+import com.nextcloud.talk.adapters.ReactionItemClickListener
+import com.nextcloud.talk.adapters.ReactionsAdapter
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.databinding.DialogMessageReactionsBinding
+import com.nextcloud.talk.databinding.ItemReactionsTabBinding
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.models.json.reactions.ReactionsOverall
+import com.nextcloud.talk.utils.ApiUtils
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import java.util.Collections
+import java.util.Comparator
+
+@AutoInjector(NextcloudTalkApplication::class)
+class ShowReactionsDialog(
+    activity: Activity,
+    private val currentConversation: Conversation?,
+    private val chatMessage: ChatMessage,
+    private val userEntity: UserEntity?,
+    private val ncApi: NcApi
+) : BottomSheetDialog(activity), ReactionItemClickListener {
+
+    private lateinit var binding: DialogMessageReactionsBinding
+
+    private var adapter: ReactionsAdapter? = null
+
+    private val tagAll: String? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = DialogMessageReactionsBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+        window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+        adapter = ReactionsAdapter(this, userEntity)
+        binding.reactionsList.adapter = adapter
+        binding.reactionsList.layoutManager = LinearLayoutManager(context)
+        initEmojiReactions()
+    }
+
+    private fun initEmojiReactions() {
+        adapter?.list?.clear()
+        if (chatMessage.reactions != null && chatMessage.reactions.isNotEmpty()) {
+            var reactionsTotal = 0
+            for ((emoji, amount) in chatMessage.reactions) {
+                reactionsTotal = reactionsTotal.plus(amount as Int)
+                val tab: TabLayout.Tab = binding.emojiReactionsTabs.newTab() // Create a new Tab names "First Tab"
+
+                val itemBinding = ItemReactionsTabBinding.inflate(layoutInflater)
+                itemBinding.reactionTab.tag = emoji
+                itemBinding.reactionIcon.text = emoji
+                itemBinding.reactionCount.text = amount.toString()
+                tab.customView = itemBinding.root
+
+                binding.emojiReactionsTabs.addTab(tab)
+            }
+
+            val tab: TabLayout.Tab = binding.emojiReactionsTabs.newTab() // Create a new Tab names "First Tab"
+
+            val itemBinding = ItemReactionsTabBinding.inflate(layoutInflater)
+            itemBinding.reactionTab.tag = tagAll
+            itemBinding.reactionIcon.text = context.getString(R.string.reactions_tab_all)
+            itemBinding.reactionCount.text = reactionsTotal.toString()
+            tab.customView = itemBinding.root
+
+            binding.emojiReactionsTabs.addTab(tab, 0)
+
+            binding.emojiReactionsTabs.getTabAt(0)?.select()
+
+            binding.emojiReactionsTabs.addOnTabSelectedListener(object : OnTabSelectedListener {
+                override fun onTabSelected(tab: TabLayout.Tab) {
+                    // called when a tab is reselected
+                    updateParticipantsForEmoji(chatMessage, tab.customView?.tag as String?)
+                }
+
+                override fun onTabUnselected(tab: TabLayout.Tab) {
+                    // called when a tab is reselected
+                }
+
+                override fun onTabReselected(tab: TabLayout.Tab) {
+                    // called when a tab is reselected
+                }
+            })
+
+            updateParticipantsForEmoji(chatMessage, tagAll)
+        }
+        adapter?.notifyDataSetChanged()
+    }
+
+    private fun updateParticipantsForEmoji(chatMessage: ChatMessage, emoji: String?) {
+        adapter?.list?.clear()
+
+        val credentials = ApiUtils.getCredentials(userEntity?.username, userEntity?.token)
+
+        ncApi.getReactions(
+            credentials,
+            ApiUtils.getUrlForMessageReaction(
+                userEntity?.baseUrl,
+                currentConversation!!.token,
+                chatMessage.id
+            ),
+            emoji
+        )
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<ReactionsOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(@NonNull reactionsOverall: ReactionsOverall) {
+                    val reactionVoters: ArrayList<ReactionItem> = ArrayList()
+                    if (reactionsOverall.ocs?.data != null) {
+                        val map = reactionsOverall.ocs?.data
+                        for (key in map!!.keys) {
+                            for (reactionVoter in reactionsOverall.ocs?.data!![key]!!) {
+                                reactionVoters.add(ReactionItem(reactionVoter, key))
+                            }
+                        }
+
+                        Collections.sort(reactionVoters, ReactionComparator(userEntity?.userId))
+
+                        adapter?.list?.addAll(reactionVoters)
+                        adapter?.notifyDataSetChanged()
+                    } else {
+                        Log.e(TAG, "no voters for this reaction")
+                    }
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "failed to retrieve list of reaction voters")
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    override fun onClick(reactionItem: ReactionItem) {
+        if (reactionItem.reactionVoter.actorId?.equals(userEntity?.userId) == true) {
+            deleteReaction(chatMessage, reactionItem.reaction!!)
+        }
+
+        dismiss()
+    }
+
+    private fun deleteReaction(message: ChatMessage, emoji: String) {
+        val credentials = ApiUtils.getCredentials(userEntity?.username, userEntity?.token)
+
+        ncApi.deleteReaction(
+            credentials,
+            ApiUtils.getUrlForMessageReaction(
+                userEntity?.baseUrl,
+                currentConversation!!.token,
+                message.id
+            ),
+            emoji
+        )
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(@NonNull genericOverall: GenericOverall) {
+                    Log.d(TAG, "deleted reaction: $emoji")
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "error while deleting reaction: $emoji")
+                }
+
+                override fun onComplete() {
+                    dismiss()
+                }
+            })
+    }
+
+    companion object {
+        const val TAG = "ShowReactionsDialog"
+    }
+
+    class ReactionComparator(val activeUser: String?) : Comparator<ReactionItem> {
+        @Suppress("ReturnCount")
+        override fun compare(reactionItem1: ReactionItem?, reactionItem2: ReactionItem?): Int {
+            // sort by emoji, own account, display-name, timestamp, actor-id
+
+            if (reactionItem1 == null && reactionItem2 == null) {
+                return 0
+            }
+            if (reactionItem1 == null) {
+                return -1
+            }
+            if (reactionItem2 == null) {
+                return 1
+            }
+
+            // emoji
+            val reaction = StringComparator().compare(reactionItem1.reaction, reactionItem2.reaction)
+            if (reaction != 0) {
+                return reaction
+            }
+
+            // own account
+            val ownAccount = compareOwnAccount(
+                activeUser,
+                reactionItem1.reactionVoter.actorId,
+                reactionItem2.reactionVoter.actorId
+            )
+
+            if (ownAccount != 0) {
+                return ownAccount
+            }
+
+            // display-name
+            val displayName = StringComparator()
+                .compare(
+                    reactionItem1.reactionVoter.actorDisplayName,
+                    reactionItem2.reactionVoter.actorDisplayName
+                )
+
+            if (displayName != 0) {
+                return displayName
+            }
+
+            // timestamp
+            val timestamp = LongComparator()
+                .compare(
+                    reactionItem1.reactionVoter.timestamp,
+                    reactionItem2.reactionVoter.timestamp
+                )
+
+            if (timestamp != 0) {
+                return timestamp
+            }
+
+            // actor-id
+            val actorId = StringComparator()
+                .compare(
+                    reactionItem1.reactionVoter.actorId,
+                    reactionItem2.reactionVoter.actorId
+                )
+
+            if (actorId != 0) {
+                return actorId
+            }
+
+            return 0
+        }
+
+        @Suppress("ReturnCount")
+        fun compareOwnAccount(activeUser: String?, actorId1: String?, actorId2: String?): Int {
+            val reactionVote1Active = activeUser == actorId1
+            val reactionVote2Active = activeUser == actorId2
+
+            if (!reactionVote1Active && !reactionVote2Active || reactionVote1Active && reactionVote2Active) {
+                return 0
+            }
+
+            if (activeUser == null) {
+                return 0
+            }
+
+            if (reactionVote1Active) {
+                return 1
+            }
+            if (reactionVote2Active) {
+                return -1
+            }
+
+            return 0
+        }
+
+        internal class StringComparator : Comparator<String?> {
+            @Suppress("ReturnCount")
+            override fun compare(obj1: String?, obj2: String?): Int {
+                if (obj1 === obj2) {
+                    return 0
+                }
+                if (obj1 == null) {
+                    return -1
+                }
+                return if (obj2 == null) {
+                    1
+                } else obj1.lowercase().compareTo(obj2.lowercase())
+            }
+        }
+
+        internal class LongComparator : Comparator<Long?> {
+            @Suppress("ReturnCount")
+            override fun compare(obj1: Long?, obj2: Long?): Int {
+                if (obj1 === obj2) {
+                    return 0
+                }
+                if (obj1 == null) {
+                    return -1
+                }
+                return if (obj2 == null) {
+                    1
+                } else obj1.compareTo(obj2)
+            }
+        }
+    }
+}

+ 7 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -441,4 +441,11 @@ public class ApiUtils {
     public static String getUrlForUserStatuses(String baseUrl) {
     public static String getUrlForUserStatuses(String baseUrl) {
         return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/statuses";
         return baseUrl + ocsApiVersion + "/apps/user_status/api/v1/statuses";
     }
     }
+
+    public static String getUrlForMessageReaction(String baseUrl,
+                                                  String roomToken,
+                                                  String messageId) {
+        return baseUrl + ocsApiVersion + spreedApiVersion + "/reaction/" + roomToken + "/" + messageId;
+    }
+
 }
 }

+ 1 - 1
app/src/main/java/com/nextcloud/talk/utils/LoggingUtils.kt

@@ -22,7 +22,7 @@ package com.nextcloud.talk.utils
 
 
 import android.content.Context
 import android.content.Context
 
 
-// TODO: improve log handling. https://github.com/nextcloud/talk-android/issues/1376
+// TODO improve log handling. https://github.com/nextcloud/talk-android/issues/1376
 // writing logs to a file is temporarily disabled to avoid huge logfiles.
 // writing logs to a file is temporarily disabled to avoid huge logfiles.
 
 
 object LoggingUtils {
 object LoggingUtils {

+ 1 - 1
app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java

@@ -243,7 +243,7 @@ public interface AppPreferences {
     @KeyByString("phone_book_integration") 
     @KeyByString("phone_book_integration") 
     void setPhoneBookIntegration(boolean value);
     void setPhoneBookIntegration(boolean value);
 
 
-    // TODO: Remove in 13.0.0
+    // TODO Remove in 13.0.0
     @KeyByString("link_previews")
     @KeyByString("link_previews")
     @RemoveMethod
     @RemoveMethod
     void removeLinkPreviews();
     void removeLinkPreviews();

+ 1 - 1
app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java

@@ -151,7 +151,7 @@ public class MagicWebSocketInstance extends WebSocketListener {
     public void restartWebSocket() {
     public void restartWebSocket() {
         reconnecting = true;
         reconnecting = true;
 
 
-        // TODO: when improving logging, keep in mind this issue: https://github.com/nextcloud/talk-android/issues/1013
+        // TODO when improving logging, keep in mind this issue: https://github.com/nextcloud/talk-android/issues/1013
         Log.d(TAG, "restartWebSocket: " + connectionUrl);
         Log.d(TAG, "restartWebSocket: " + connectionUrl);
         Request request = new Request.Builder().url(connectionUrl).build();
         Request request = new Request.Builder().url(connectionUrl).build();
         okHttpClient.newWebSocket(request, this);
         okHttpClient.newWebSocket(request, this);

+ 10 - 4
app/src/main/res/drawable/ic_delete.xml

@@ -18,8 +18,14 @@
   ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   -->
   -->
 
 
-<vector android:autoMirrored="true" android:height="24dp"
-    android:viewportHeight="24.0" android:viewportWidth="24.0"
-    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
-    <path android:fillColor="@color/medium_emphasis_text" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="@color/medium_emphasis_text"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
 </vector>
 </vector>

+ 26 - 0
app/src/main/res/drawable/ic_dots_horizontal.xml

@@ -0,0 +1,26 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2021 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="@color/high_emphasis_menu_icon"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z" />
+</vector>

+ 0 - 1
app/src/main/res/layout/bottom_sheet.xml

@@ -23,7 +23,6 @@
     android:id="@+id/bottom_sheet"
     android:id="@+id/bottom_sheet"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@color/bg_bottom_sheet"
     app:layout_behavior="@string/appbar_scrolling_view_behavior">
     app:layout_behavior="@string/appbar_scrolling_view_behavior">
 
 
 
 

+ 0 - 1
app/src/main/res/layout/controller_entry_menu.xml

@@ -23,7 +23,6 @@
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@color/bg_bottom_sheet"
     android:paddingStart="@dimen/standard_padding"
     android:paddingStart="@dimen/standard_padding"
     android:paddingTop="@dimen/standard_padding"
     android:paddingTop="@dimen/standard_padding"
     android:paddingEnd="@dimen/standard_half_padding">
     android:paddingEnd="@dimen/standard_half_padding">

+ 5 - 6
app/src/main/res/layout/dialog_attachment.xml

@@ -25,7 +25,6 @@
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@color/bg_bottom_sheet"
     android:orientation="vertical"
     android:orientation="vertical"
     android:paddingStart="@dimen/standard_padding"
     android:paddingStart="@dimen/standard_padding"
     android:paddingEnd="@dimen/standard_padding"
     android:paddingEnd="@dimen/standard_padding"
@@ -56,7 +55,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/ic_baseline_person_24"
             android:src="@drawable/ic_baseline_person_24"
-            app:tint="@color/colorPrimary" />
+            app:tint="@color/high_emphasis_menu_icon" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/shareContactText"
             android:id="@+id/shareContactText"
@@ -87,7 +86,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/ic_baseline_location_on_24"
             android:src="@drawable/ic_baseline_location_on_24"
-            app:tint="@color/colorPrimary" />
+            app:tint="@color/high_emphasis_menu_icon" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/txt_share_location"
             android:id="@+id/txt_share_location"
@@ -118,7 +117,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/ic_baseline_photo_camera_24"
             android:src="@drawable/ic_baseline_photo_camera_24"
-            app:tint="@color/colorPrimary" />
+            app:tint="@color/high_emphasis_menu_icon" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/txt_attach_picture_from_cam"
             android:id="@+id/txt_attach_picture_from_cam"
@@ -149,7 +148,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/upload"
             android:src="@drawable/upload"
-            app:tint="@color/colorPrimary" />
+            app:tint="@color/high_emphasis_menu_icon" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/txt_attach_file_from_local"
             android:id="@+id/txt_attach_file_from_local"
@@ -180,7 +179,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/ic_share_variant"
             android:src="@drawable/ic_share_variant"
-            app:tint="@color/colorPrimary" />
+            app:tint="@color/high_emphasis_menu_icon" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/txt_attach_file_from_cloud"
             android:id="@+id/txt_attach_file_from_cloud"

+ 6 - 6
app/src/main/res/layout/dialog_audio_output.xml

@@ -49,11 +49,11 @@
 
 
         <ImageView
         <ImageView
             android:id="@+id/audio_output_bluetooth_icon"
             android:id="@+id/audio_output_bluetooth_icon"
-            android:layout_width="11dp"
-            android:layout_height="12dp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/ic_baseline_bluetooth_audio_24"
             android:src="@drawable/ic_baseline_bluetooth_audio_24"
-            app:tint="@color/grey_600" />
+            app:tint="@color/high_emphasis_menu_icon_inverse" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/audio_output_bluetooth_text"
             android:id="@+id/audio_output_bluetooth_text"
@@ -84,7 +84,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/ic_volume_up_white_24dp"
             android:src="@drawable/ic_volume_up_white_24dp"
-            app:tint="@color/grey_600" />
+            app:tint="@color/high_emphasis_menu_icon_inverse" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/audio_output_speaker_text"
             android:id="@+id/audio_output_speaker_text"
@@ -115,7 +115,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/ic_baseline_phone_in_talk_24"
             android:src="@drawable/ic_baseline_phone_in_talk_24"
-            app:tint="@color/grey_600" />
+            app:tint="@color/high_emphasis_menu_icon_inverse" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/audio_output_earspeaker_text"
             android:id="@+id/audio_output_earspeaker_text"
@@ -146,7 +146,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:contentDescription="@null"
             android:contentDescription="@null"
             android:src="@drawable/ic_baseline_headset_mic_24"
             android:src="@drawable/ic_baseline_headset_mic_24"
-            app:tint="@color/grey_600" />
+            app:tint="@color/high_emphasis_menu_icon_inverse" />
 
 
         <androidx.appcompat.widget.AppCompatTextView
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/audio_output_wired_headset_text"
             android:id="@+id/audio_output_wired_headset_text"

+ 0 - 1
app/src/main/res/layout/dialog_bottom_contacts.xml

@@ -23,7 +23,6 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@color/bg_bottom_sheet"
     android:orientation="vertical"
     android:orientation="vertical"
     android:paddingStart="@dimen/standard_padding"
     android:paddingStart="@dimen/standard_padding"
     android:paddingEnd="@dimen/standard_padding"
     android:paddingEnd="@dimen/standard_padding"

+ 12 - 13
app/src/main/res/layout/dialog_conversation_operations.xml

@@ -25,7 +25,6 @@
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@color/bg_bottom_sheet"
     android:orientation="vertical"
     android:orientation="vertical"
     android:paddingStart="@dimen/standard_padding"
     android:paddingStart="@dimen/standard_padding"
     android:paddingEnd="@dimen/standard_padding"
     android:paddingEnd="@dimen/standard_padding"
@@ -60,7 +59,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_star_border_black_24dp"
                 android:src="@drawable/ic_star_border_black_24dp"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -87,7 +86,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_star_black_24dp"
                 android:src="@drawable/ic_star_black_24dp"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -114,7 +113,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_eye"
                 android:src="@drawable/ic_eye"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -141,7 +140,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_pencil_grey600_24dp"
                 android:src="@drawable/ic_pencil_grey600_24dp"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -168,7 +167,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_link_grey600_24px"
                 android:src="@drawable/ic_link_grey600_24px"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -195,7 +194,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_lock_grey600_24px"
                 android:src="@drawable/ic_lock_grey600_24px"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -222,7 +221,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_lock_open_grey600_24dp"
                 android:src="@drawable/ic_lock_open_grey600_24dp"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -249,7 +248,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_lock_plus_grey600_24dp"
                 android:src="@drawable/ic_lock_plus_grey600_24dp"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -276,7 +275,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_delete_grey600_24dp"
                 android:src="@drawable/ic_delete_grey600_24dp"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -303,7 +302,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_link_grey600_24px"
                 android:src="@drawable/ic_link_grey600_24px"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -330,7 +329,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_group_grey600_24px"
                 android:src="@drawable/ic_group_grey600_24px"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"
@@ -357,7 +356,7 @@
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
                 android:contentDescription="@null"
                 android:contentDescription="@null"
                 android:src="@drawable/ic_exit_to_app_black_24dp"
                 android:src="@drawable/ic_exit_to_app_black_24dp"
-                app:tint="@color/grey_600" />
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
             <androidx.appcompat.widget.AppCompatTextView
             <androidx.appcompat.widget.AppCompatTextView
                 android:layout_width="match_parent"
                 android:layout_width="match_parent"

+ 252 - 164
app/src/main/res/layout/dialog_message_actions.xml

@@ -23,195 +23,283 @@
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@color/bg_bottom_sheet"
     android:orientation="vertical"
     android:orientation="vertical"
-    android:paddingStart="@dimen/standard_padding"
-    android:paddingEnd="@dimen/standard_padding"
     android:paddingBottom="@dimen/standard_half_padding">
     android:paddingBottom="@dimen/standard_half_padding">
 
 
     <LinearLayout
     <LinearLayout
-        android:id="@+id/menu_copy_message"
+        android:id="@+id/emojiBar"
         android:layout_width="match_parent"
         android:layout_width="match_parent"
         android:layout_height="@dimen/bottom_sheet_item_height"
         android:layout_height="@dimen/bottom_sheet_item_height"
-        android:background="?android:attr/selectableItemBackground"
+        android:layout_marginStart="@dimen/standard_eighth_margin"
+        android:layout_marginEnd="@dimen/zero"
         android:gravity="center_vertical"
         android:gravity="center_vertical"
-        android:orientation="horizontal"
-        tools:ignore="UseCompoundDrawables">
-
-        <ImageView
-            android:id="@+id/menu_icon_copy_message"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_content_copy"
-            app:tint="@color/grey_600" />
-
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/menu_text_copy_message"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start|center_vertical"
-            android:paddingStart="@dimen/standard_double_padding"
-            android:paddingEnd="@dimen/zero"
-            android:text="@string/nc_copy_message"
-            android:textAlignment="viewStart"
-            android:textColor="@color/high_emphasis_text"
-            android:textSize="@dimen/bottom_sheet_text_size" />
+        android:orientation="horizontal">
+
+        <com.vanniktech.emoji.EmojiTextView
+            android:id="@+id/emojiThumbsUp"
+            android:layout_width="@dimen/activity_row_layout_height"
+            android:layout_height="@dimen/activity_row_layout_height"
+            android:layout_weight="1"
+            android:cursorVisible="false"
+            android:gravity="center"
+            android:text="@string/emoji_thumbsUp"
+            android:textSize="24sp" />
+
+        <com.vanniktech.emoji.EmojiTextView
+            android:id="@+id/emojiThumbsDown"
+            android:layout_width="@dimen/activity_row_layout_height"
+            android:layout_height="@dimen/activity_row_layout_height"
+            android:layout_weight="1"
+            android:cursorVisible="false"
+            android:gravity="center"
+            android:text="@string/emoji_thumbsDown"
+            android:textSize="24sp" />
+
+        <com.vanniktech.emoji.EmojiTextView
+            android:id="@+id/emojiHeart"
+            android:layout_width="@dimen/activity_row_layout_height"
+            android:layout_height="@dimen/activity_row_layout_height"
+            android:layout_weight="1"
+            android:cursorVisible="false"
+            android:gravity="center"
+            android:text="@string/default_emoji"
+            android:textSize="24sp" />
+
+        <com.vanniktech.emoji.EmojiTextView
+            android:id="@+id/emojiLaugh"
+            android:layout_width="@dimen/activity_row_layout_height"
+            android:layout_height="@dimen/activity_row_layout_height"
+            android:layout_weight="1"
+            android:cursorVisible="false"
+            android:gravity="center"
+            android:text="@string/emoji_heart"
+            android:textSize="24sp" />
+
+        <com.vanniktech.emoji.EmojiTextView
+            android:id="@+id/emojiConfused"
+            android:layout_width="@dimen/activity_row_layout_height"
+            android:layout_height="@dimen/activity_row_layout_height"
+            android:layout_weight="1"
+            android:cursorVisible="false"
+            android:gravity="center"
+            android:text="@string/emoji_confused"
+            android:textSize="24sp" />
 
 
+        <com.vanniktech.emoji.EmojiTextView
+            android:id="@+id/emojiSad"
+            android:layout_width="@dimen/activity_row_layout_height"
+            android:layout_height="@dimen/activity_row_layout_height"
+            android:layout_weight="1"
+            android:cursorVisible="false"
+            android:gravity="center"
+            android:text="@string/emoji_sad"
+            android:textSize="24sp" />
+
+        <com.vanniktech.emoji.EmojiEditText
+            android:id="@+id/emojiMore"
+            android:layout_width="@dimen/activity_row_layout_height"
+            android:layout_height="@dimen/activity_row_layout_height"
+            android:layout_weight="1"
+            android:background="@android:color/transparent"
+            android:contentDescription="@string/emoji_more"
+            android:drawableEnd="@drawable/ic_dots_horizontal"
+            android:paddingStart="@dimen/zero"
+            android:paddingEnd="@dimen/standard_padding" />
     </LinearLayout>
     </LinearLayout>
 
 
     <LinearLayout
     <LinearLayout
-        android:id="@+id/menu_mark_as_unread"
+        android:id="@+id/message_actions"
         android:layout_width="match_parent"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/bottom_sheet_item_height"
-        android:background="?android:attr/selectableItemBackground"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        tools:ignore="UseCompoundDrawables">
-
-        <ImageView
-            android:id="@+id/menu_icon_mark_as_unread"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_eye_off"
-            app:tint="@color/grey_600" />
-
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/menu_text_mark_as_unread"
+        android:layout_height="wrap_content"
+        android:paddingStart="@dimen/standard_padding"
+        android:paddingEnd="@dimen/standard_padding"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:id="@+id/menu_reply_to_message"
             android:layout_width="match_parent"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start|center_vertical"
-            android:paddingStart="@dimen/standard_double_padding"
-            android:paddingEnd="@dimen/zero"
-            android:text="@string/nc_mark_as_unread"
-            android:textAlignment="viewStart"
-            android:textColor="@color/high_emphasis_text"
-            android:textSize="@dimen/bottom_sheet_text_size" />
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="UseCompoundDrawables">
 
 
-    </LinearLayout>
+            <ImageView
+                android:id="@+id/menu_icon_reply_to_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_reply"
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
-    <LinearLayout
-        android:id="@+id/menu_forward_message"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/bottom_sheet_item_height"
-        android:background="?android:attr/selectableItemBackground"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        tools:ignore="UseCompoundDrawables">
-
-        <ImageView
-            android:id="@+id/menu_icon_forward_message"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_share_action"
-            app:tint="@color/grey_600" />
-
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/menu_text_forward_message"
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/menu_text_reply_to_message"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="@dimen/standard_double_padding"
+                android:paddingEnd="@dimen/zero"
+                android:text="@string/nc_reply"
+                android:textAlignment="viewStart"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="@dimen/bottom_sheet_text_size" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/menu_reply_privately"
             android:layout_width="match_parent"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start|center_vertical"
-            android:paddingStart="@dimen/standard_double_padding"
-            android:paddingEnd="@dimen/zero"
-            android:text="@string/nc_forward_message"
-            android:textAlignment="viewStart"
-            android:textColor="@color/high_emphasis_text"
-            android:textSize="@dimen/bottom_sheet_text_size" />
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="UseCompoundDrawables">
 
 
-    </LinearLayout>
+            <ImageView
+                android:id="@+id/menu_icon_reply_privately"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_reply"
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
-    <LinearLayout
-        android:id="@+id/menu_reply_to_message"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/bottom_sheet_item_height"
-        android:background="?android:attr/selectableItemBackground"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        tools:ignore="UseCompoundDrawables">
-
-        <ImageView
-            android:id="@+id/menu_icon_reply_to_message"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_reply"
-            app:tint="@color/grey_600" />
-
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/menu_text_reply_to_message"
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/menu_text_reply_privately"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="@dimen/standard_double_padding"
+                android:paddingEnd="@dimen/zero"
+                android:text="@string/nc_reply_privately"
+                android:textAlignment="viewStart"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="@dimen/bottom_sheet_text_size" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/menu_forward_message"
             android:layout_width="match_parent"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start|center_vertical"
-            android:paddingStart="@dimen/standard_double_padding"
-            android:paddingEnd="@dimen/zero"
-            android:text="@string/nc_reply"
-            android:textAlignment="viewStart"
-            android:textColor="@color/high_emphasis_text"
-            android:textSize="@dimen/bottom_sheet_text_size" />
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="UseCompoundDrawables">
 
 
-    </LinearLayout>
+            <ImageView
+                android:id="@+id/menu_icon_forward_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_share_action"
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
-    <LinearLayout
-        android:id="@+id/menu_reply_privately"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/bottom_sheet_item_height"
-        android:background="?android:attr/selectableItemBackground"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        tools:ignore="UseCompoundDrawables">
-
-        <ImageView
-            android:id="@+id/menu_icon_reply_privately"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_reply"
-            app:tint="@color/grey_600" />
-
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/menu_text_reply_privately"
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/menu_text_forward_message"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="@dimen/standard_double_padding"
+                android:paddingEnd="@dimen/zero"
+                android:text="@string/nc_forward_message"
+                android:textAlignment="viewStart"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="@dimen/bottom_sheet_text_size" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/menu_mark_as_unread"
             android:layout_width="match_parent"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start|center_vertical"
-            android:paddingStart="@dimen/standard_double_padding"
-            android:paddingEnd="@dimen/zero"
-            android:text="@string/nc_reply_privately"
-            android:textAlignment="viewStart"
-            android:textColor="@color/high_emphasis_text"
-            android:textSize="@dimen/bottom_sheet_text_size" />
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="UseCompoundDrawables">
 
 
-    </LinearLayout>
+            <ImageView
+                android:id="@+id/menu_icon_mark_as_unread"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_eye_off"
+                app:tint="@color/high_emphasis_menu_icon" />
 
 
-    <LinearLayout
-        android:id="@+id/menu_delete_message"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/bottom_sheet_item_height"
-        android:background="?android:attr/selectableItemBackground"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        tools:ignore="UseCompoundDrawables">
-
-        <ImageView
-            android:id="@+id/menu_icon_delete_message"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_delete"
-            app:tint="@color/grey_600" />
-
-        <androidx.appcompat.widget.AppCompatTextView
-            android:id="@+id/menu_text_delete_message"
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/menu_text_mark_as_unread"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="@dimen/standard_double_padding"
+                android:paddingEnd="@dimen/zero"
+                android:text="@string/nc_mark_as_unread"
+                android:textAlignment="viewStart"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="@dimen/bottom_sheet_text_size" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/menu_copy_message"
             android:layout_width="match_parent"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start|center_vertical"
-            android:paddingStart="@dimen/standard_double_padding"
-            android:paddingEnd="@dimen/zero"
-            android:text="@string/nc_delete_message"
-            android:textAlignment="viewStart"
-            android:textColor="@color/high_emphasis_text"
-            android:textSize="@dimen/bottom_sheet_text_size" />
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="UseCompoundDrawables">
+
+            <ImageView
+                android:id="@+id/menu_icon_copy_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_content_copy"
+                app:tint="@color/high_emphasis_menu_icon" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/menu_text_copy_message"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="@dimen/standard_double_padding"
+                android:paddingEnd="@dimen/zero"
+                android:text="@string/nc_copy_message"
+                android:textAlignment="viewStart"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="@dimen/bottom_sheet_text_size" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/menu_delete_message"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/bottom_sheet_item_height"
+            android:background="?android:attr/selectableItemBackground"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="UseCompoundDrawables">
+
+            <ImageView
+                android:id="@+id/menu_icon_delete_message"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:contentDescription="@null"
+                android:src="@drawable/ic_delete"
+                app:tint="@color/high_emphasis_menu_icon" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/menu_text_delete_message"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:paddingStart="@dimen/standard_double_padding"
+                android:paddingEnd="@dimen/zero"
+                android:text="@string/nc_delete_message"
+                android:textAlignment="viewStart"
+                android:textColor="@color/high_emphasis_text"
+                android:textSize="@dimen/bottom_sheet_text_size" />
+
+        </LinearLayout>
 
 
     </LinearLayout>
     </LinearLayout>
 
 

+ 48 - 0
app/src/main/res/layout/dialog_message_reactions.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Andy Scherzinger
+  ~ @author Marcel Hibbe
+  ~ Copyright (C) 2022 Andy Scherzinger
+  ~ Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="288dp">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/reactions_list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            tools:listitem="@layout/reaction_item" />
+    </LinearLayout>
+
+    <com.google.android.material.tabs.TabLayout
+        android:id="@+id/emoji_reactions_tabs"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/min_size_clickable_area"
+        app:tabGravity="fill"
+        app:tabMode="scrollable" />
+
+</LinearLayout>

+ 0 - 1
app/src/main/res/layout/dialog_scope.xml

@@ -24,7 +24,6 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@color/bg_bottom_sheet"
     android:orientation="vertical"
     android:orientation="vertical"
     android:paddingStart="@dimen/standard_padding"
     android:paddingStart="@dimen/standard_padding"
     android:paddingTop="@dimen/standard_half_padding"
     android:paddingTop="@dimen/standard_half_padding"

+ 4 - 1
app/src/main/res/layout/item_custom_incoming_location_message.xml

@@ -76,7 +76,7 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:lineSpacingMultiplier="1.2"
             android:lineSpacingMultiplier="1.2"
             android:textAlignment="viewStart"
             android:textAlignment="viewStart"
-            android:textIsSelectable="true"
+            android:textIsSelectable="false"
             app:layout_alignSelf="flex_start"
             app:layout_alignSelf="flex_start"
             app:layout_flexGrow="1"
             app:layout_flexGrow="1"
             app:layout_wrapBefore="true" />
             app:layout_wrapBefore="true" />
@@ -89,5 +89,8 @@
             android:layout_marginStart="8dp"
             android:layout_marginStart="8dp"
             app:layout_alignSelf="center" />
             app:layout_alignSelf="center" />
 
 
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
     </com.google.android.flexbox.FlexboxLayout>
     </com.google.android.flexbox.FlexboxLayout>
 </RelativeLayout>
 </RelativeLayout>

+ 4 - 0
app/src/main/res/layout/item_custom_incoming_preview_message.xml

@@ -173,6 +173,10 @@
             android:textColor="@color/warm_grey_four"
             android:textColor="@color/warm_grey_four"
             app:layout_alignSelf="center"
             app:layout_alignSelf="center"
             tools:text="12:38" />
             tools:text="12:38" />
+
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
     </com.google.android.flexbox.FlexboxLayout>
     </com.google.android.flexbox.FlexboxLayout>
 
 
 </RelativeLayout>
 </RelativeLayout>

+ 5 - 2
app/src/main/res/layout/item_custom_incoming_text_message.xml

@@ -47,8 +47,7 @@
         android:orientation="vertical"
         android:orientation="vertical"
         app:alignContent="stretch"
         app:alignContent="stretch"
         app:alignItems="stretch"
         app:alignItems="stretch"
-        app:flexWrap="wrap"
-        app:justifyContent="flex_end">
+        app:flexWrap="wrap">
 
 
         <include
         <include
             android:id="@+id/message_quote"
             android:id="@+id/message_quote"
@@ -87,5 +86,9 @@
             android:textIsSelectable="false"
             android:textIsSelectable="false"
             app:layout_alignSelf="center" />
             app:layout_alignSelf="center" />
 
 
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
     </com.google.android.flexbox.FlexboxLayout>
     </com.google.android.flexbox.FlexboxLayout>
 </RelativeLayout>
 </RelativeLayout>

+ 4 - 0
app/src/main/res/layout/item_custom_incoming_voice_message.xml

@@ -108,5 +108,9 @@
                     app:layout_alignSelf="center"
                     app:layout_alignSelf="center"
                     tools:text="12:38"/>
                     tools:text="12:38"/>
 
 
+            <include
+                android:id="@+id/reactions"
+                layout="@layout/reactions_inside_message" />
+
         </com.google.android.flexbox.FlexboxLayout>
         </com.google.android.flexbox.FlexboxLayout>
 </RelativeLayout>
 </RelativeLayout>

+ 4 - 1
app/src/main/res/layout/item_custom_outcoming_location_message.xml

@@ -59,7 +59,7 @@
             android:lineSpacingMultiplier="1.2"
             android:lineSpacingMultiplier="1.2"
             android:textAlignment="viewStart"
             android:textAlignment="viewStart"
             android:textColorHighlight="@color/nc_grey"
             android:textColorHighlight="@color/nc_grey"
-            android:textIsSelectable="true"
+            android:textIsSelectable="false"
             tools:text="Talk to you later!" />
             tools:text="Talk to you later!" />
 
 
         <TextView
         <TextView
@@ -80,5 +80,8 @@
             app:layout_alignSelf="center"
             app:layout_alignSelf="center"
             android:contentDescription="@null" />
             android:contentDescription="@null" />
 
 
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
     </com.google.android.flexbox.FlexboxLayout>
     </com.google.android.flexbox.FlexboxLayout>
 </RelativeLayout>
 </RelativeLayout>

+ 4 - 0
app/src/main/res/layout/item_custom_outcoming_preview_message.xml

@@ -163,6 +163,10 @@
             android:textColor="@color/warm_grey_four"
             android:textColor="@color/warm_grey_four"
             app:layout_alignSelf="center"
             app:layout_alignSelf="center"
             tools:text="12:34" />
             tools:text="12:34" />
+
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
     </com.google.android.flexbox.FlexboxLayout>
     </com.google.android.flexbox.FlexboxLayout>
 
 
 </RelativeLayout>
 </RelativeLayout>

+ 4 - 0
app/src/main/res/layout/item_custom_outcoming_text_message.xml

@@ -76,5 +76,9 @@
             android:contentDescription="@null"
             android:contentDescription="@null"
             app:layout_alignSelf="center" />
             app:layout_alignSelf="center" />
 
 
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
     </com.google.android.flexbox.FlexboxLayout>
     </com.google.android.flexbox.FlexboxLayout>
 </RelativeLayout>
 </RelativeLayout>

+ 4 - 0
app/src/main/res/layout/item_custom_outcoming_voice_message.xml

@@ -104,5 +104,9 @@
             app:layout_alignSelf="center"
             app:layout_alignSelf="center"
             android:contentDescription="@null" />
             android:contentDescription="@null" />
 
 
+        <include
+            android:id="@+id/reactions"
+            layout="@layout/reactions_inside_message" />
+
     </com.google.android.flexbox.FlexboxLayout>
     </com.google.android.flexbox.FlexboxLayout>
 </RelativeLayout>
 </RelativeLayout>

+ 46 - 0
app/src/main/res/layout/item_reactions_tab.xml

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/reaction_tab"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center">
+
+    <androidx.emoji.widget.EmojiTextView
+        android:id="@+id/reaction_icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:textIsSelectable="false"
+        android:textSize="14sp"
+        android:textStyle="bold"
+        tools:text="@string/default_emoji" />
+
+    <TextView
+        android:id="@+id/reaction_count"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="14sp"
+        android:textStyle="bold"
+        tools:text="1" />
+
+</LinearLayout>

+ 56 - 0
app/src/main/res/layout/reaction_item.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+
+  Nextcloud Talk application
+
+  Copyright (C) 2022 Andy Scherzinger
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU Affero General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU Affero General Public License for more details.
+
+  You should have received a copy of the GNU Affero General Public License
+  along with this program. If not, see <https://www.gnu.org/licenses/>.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/item_height">
+
+    <com.facebook.drawee.view.SimpleDraweeView
+        android:id="@+id/avatar"
+        android:layout_width="@dimen/avatar_size"
+        android:layout_height="@dimen/avatar_size"
+        android:layout_gravity="center_vertical"
+        android:layout_margin="@dimen/standard_margin"
+        app:roundAsCircle="true" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:ellipsize="middle"
+        android:gravity="center_vertical|start"
+        android:textColor="@color/high_emphasis_text"
+        android:textSize="@dimen/bottom_sheet_text_size"
+        tools:text="Participant Name" />
+
+    <androidx.emoji.widget.EmojiTextView
+        android:id="@+id/reaction"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_gravity="center_vertical"
+        android:gravity="center"
+        android:textSize="24sp"
+        android:layout_marginEnd="@dimen/standard_half_margin"
+        tools:text="@string/default_emoji" />
+
+</LinearLayout>

+ 36 - 0
app/src/main/res/layout/reactions_inside_message.xml

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+
+  Nextcloud Talk application
+
+  Copyright (C) 2022 Marcel Hibbe
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU Affero General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU Affero General Public License for more details.
+
+  You should have received a copy of the GNU Affero General Public License
+  along with this program. If not, see <https://www.gnu.org/licenses/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/reactions_emoji_wrapper"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="5dp"
+    app:layout_alignSelf="flex_start"
+    app:layout_flexGrow="1"
+    app:layout_wrapBefore="true">
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        tools:text="emojis">
+    </TextView>
+</LinearLayout>

+ 4 - 2
app/src/main/res/values-night/colors.xml

@@ -38,7 +38,9 @@
     <color name="high_emphasis_text">#deffffff</color>
     <color name="high_emphasis_text">#deffffff</color>
     <color name="medium_emphasis_text">#99ffffff</color>
     <color name="medium_emphasis_text">#99ffffff</color>
     <color name="low_emphasis_text">#61ffffff</color>
     <color name="low_emphasis_text">#61ffffff</color>
-    <color name="high_emphasis_text_inverse">#de000000</color>
+
+    <!-- bottom sheet specific icon default color -->
+    <color name="high_emphasis_menu_icon">#8Affffff</color>
 
 
     <color name="bg_default">#121212</color>
     <color name="bg_default">#121212</color>
     <color name="bg_default_semitransparent">#99121212</color>
     <color name="bg_default_semitransparent">#99121212</color>
@@ -58,7 +60,7 @@
 
 
     <!-- Chat window incoming message text & informational -->
     <!-- Chat window incoming message text & informational -->
     <color name="bg_bottom_sheet">#121212</color>
     <color name="bg_bottom_sheet">#121212</color>
-    <color name="bg_message_list_incoming_bubble">#1CFFFFFF</color>
+    <color name="bg_message_list_incoming_bubble">#2A2A2A</color>
     <color name="bg_message_list_incoming_bubble_deleted">#14FFFFFF</color>
     <color name="bg_message_list_incoming_bubble_deleted">#14FFFFFF</color>
 
 
     <color name="textColorMaxContrast">#8c8c8c</color>
     <color name="textColorMaxContrast">#8c8c8c</color>

+ 5 - 2
app/src/main/res/values/colors.xml

@@ -39,12 +39,15 @@
     <color name="high_emphasis_text">#de000000</color>
     <color name="high_emphasis_text">#de000000</color>
     <color name="medium_emphasis_text">#99000000</color>
     <color name="medium_emphasis_text">#99000000</color>
     <color name="low_emphasis_text">#61000000</color>
     <color name="low_emphasis_text">#61000000</color>
-    <color name="high_emphasis_text_inverse">#deffffff</color>
 
 
     <!-- general text colors for dark background -->
     <!-- general text colors for dark background -->
     <color name="high_emphasis_text_dark_background">#deffffff</color>
     <color name="high_emphasis_text_dark_background">#deffffff</color>
     <color name="medium_emphasis_text_dark_background">#99ffffff</color>
     <color name="medium_emphasis_text_dark_background">#99ffffff</color>
 
 
+    <!-- bottom sheet specific icon default color -->
+    <color name="high_emphasis_menu_icon_inverse">#8Affffff</color>
+    <color name="high_emphasis_menu_icon">#8A000000</color>
+
     <!-- Text color of sent messages -->
     <!-- Text color of sent messages -->
     <color name="nc_outcoming_text_default">#FFFFFF</color>
     <color name="nc_outcoming_text_default">#FFFFFF</color>
     <!-- Text color of received messages -->
     <!-- Text color of received messages -->
@@ -82,7 +85,7 @@
     <color name="bg_message_list_outcoming_bubble">@color/colorPrimary</color>
     <color name="bg_message_list_outcoming_bubble">@color/colorPrimary</color>
     <color name="bg_message_list_outcoming_bubble_deleted">#800082C9</color>
     <color name="bg_message_list_outcoming_bubble_deleted">#800082C9</color>
 
 
-    <color name="bg_bottom_sheet">#46ffffff</color>
+    <color name="bg_bottom_sheet">#FFFFFF</color>
     <color name="bg_call_screen_dialog">#121212</color>
     <color name="bg_call_screen_dialog">#121212</color>
     <color name="call_screen_text">#ffffffff</color>
     <color name="call_screen_text">#ffffffff</color>
 
 

+ 8 - 0
app/src/main/res/values/strings.xml

@@ -279,6 +279,12 @@
     <string name="invisible">Invisible</string>
     <string name="invisible">Invisible</string>
     <string translatable="false" name="divider">—</string>
     <string translatable="false" name="divider">—</string>
     <string translatable="false" name="default_emoji">😃</string>
     <string translatable="false" name="default_emoji">😃</string>
+    <string translatable="false" name="emoji_thumbsUp">👍</string>
+    <string translatable="false" name="emoji_thumbsDown">👎</string>
+    <string translatable="false" name="emoji_heart">❤️</string>
+    <string translatable="false" name="emoji_confused">😯</string>
+    <string translatable="false" name="emoji_sad">😢</string>
+    <string translatable="false" name="emoji_more">More emojis</string>
     <string name="dontClear">Don\'t clear</string>
     <string name="dontClear">Don\'t clear</string>
     <string name="today">Today</string>
     <string name="today">Today</string>
     <string name="thirtyMinutes">30 minutes</string>
     <string name="thirtyMinutes">30 minutes</string>
@@ -507,4 +513,6 @@
     <string name="audio_output_dialog_headline">Audio output</string>
     <string name="audio_output_dialog_headline">Audio output</string>
     <string name="audio_output_wired_headset">Wired headset</string>
     <string name="audio_output_wired_headset">Wired headset</string>
 
 
+    <string name="reactions_tab_all">All</string>
+
 </resources>
 </resources>

+ 10 - 1
app/src/main/res/values/styles.xml

@@ -40,6 +40,7 @@
         <item name="android:navigationBarColor">@color/bg_default</item>
         <item name="android:navigationBarColor">@color/bg_default</item>
         <item name="android:seekBarStyle">@style/Nextcloud.Material.Incoming.SeekBar</item>
         <item name="android:seekBarStyle">@style/Nextcloud.Material.Incoming.SeekBar</item>
         <item name="seekBarStyle">@style/Nextcloud.Material.Incoming.SeekBar</item>
         <item name="seekBarStyle">@style/Nextcloud.Material.Incoming.SeekBar</item>
+        <item name="bottomSheetDialogTheme">@style/ThemeOverlay.App.BottomSheetDialog</item>
     </style>
     </style>
 
 
     <style name="ThemeOverlay.AppTheme.PopupMenu" parent="ThemeOverlay.MaterialComponents.Dark">
     <style name="ThemeOverlay.AppTheme.PopupMenu" parent="ThemeOverlay.MaterialComponents.Dark">
@@ -55,6 +56,14 @@
         <item name="elevation">1dp</item>
         <item name="elevation">1dp</item>
     </style>
     </style>
 
 
+    <style name="ThemeOverlay.App.BottomSheetDialog" parent="ThemeOverlay.MaterialComponents.DayNight.BottomSheetDialog">
+        <item name="bottomSheetStyle">@style/Talk.BottomSheetDialog</item>
+    </style>
+
+    <style name="Talk.BottomSheetDialog" parent="Widget.MaterialComponents.BottomSheet.Modal">
+        <item name="backgroundTint">@color/bg_bottom_sheet</item>
+    </style>
+
     <style name="TransparentTheme" parent="Theme.MaterialComponents.NoActionBar.Bridge">
     <style name="TransparentTheme" parent="Theme.MaterialComponents.NoActionBar.Bridge">
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowBackground">@android:color/background_dark</item>
         <item name="android:windowBackground">@android:color/background_dark</item>
@@ -235,7 +244,7 @@
         <item name="android:colorControlNormal">#ffffff</item>
         <item name="android:colorControlNormal">#ffffff</item>
     </style>
     </style>
 
 
-    <style name="BottomSheetDialogThemeNoFloating" parent="Theme.Design.Light.BottomSheetDialog">
+    <style name="BottomSheetDialogThemeNoFloating" parent="ThemeOverlay.MaterialComponents.DayNight.BottomSheetDialog">
         <item name="android:windowIsFloating">false</item>
         <item name="android:windowIsFloating">false</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
     </style>
     </style>

+ 1 - 1
detekt.yml

@@ -1,5 +1,5 @@
 build:
 build:
-  maxIssues: 98
+  maxIssues: 95
   weights:
   weights:
     # complexity: 2
     # complexity: 2
     # LongParameterList: 1
     # LongParameterList: 1

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 1 error and 164 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 1 error and 156 warnings</span>