Просмотр исходного кода

Merge pull request #1894 from nextcloud/feature/noid/movePopupMenuToBottomSheet

Move popup menu to custom bottom sheet
Marcel Hibbe 3 лет назад
Родитель
Сommit
108a568f66

+ 196 - 207
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -30,6 +30,7 @@ import android.Manifest
 import android.annotation.SuppressLint
 import android.app.Activity.RESULT_OK
 import android.content.ClipData
+import android.content.ClipboardManager
 import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
@@ -55,7 +56,6 @@ import android.text.TextUtils
 import android.text.TextWatcher
 import android.util.Log
 import android.util.TypedValue
-import android.view.Gravity
 import android.view.Menu
 import android.view.MenuInflater
 import android.view.MenuItem
@@ -67,10 +67,8 @@ import android.view.animation.LinearInterpolator
 import android.widget.AbsListView
 import android.widget.ImageButton
 import android.widget.ImageView
-import android.widget.PopupMenu
 import android.widget.RelativeLayout
 import android.widget.Toast
-import androidx.appcompat.view.ContextThemeWrapper
 import androidx.core.content.ContextCompat
 import androidx.core.content.FileProvider
 import androidx.core.content.PermissionChecker
@@ -140,6 +138,7 @@ import com.nextcloud.talk.models.json.mention.Mention
 import com.nextcloud.talk.presenters.MentionAutocompletePresenter
 import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
 import com.nextcloud.talk.ui.dialog.AttachmentDialog
+import com.nextcloud.talk.ui.dialog.MessageActionsDialog
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.utils.ApiUtils
@@ -579,7 +578,7 @@ class ChatController(args: Bundle) :
                 object : MessageSwipeActions {
                     override fun showReplyUI(position: Int) {
                         val chatMessage = adapter?.items?.get(position)?.item as ChatMessage?
-                        replyToMessage(chatMessage, chatMessage?.jsonMessageId)
+                        replyToMessage(chatMessage)
                     }
                 }
             )
@@ -2400,224 +2399,214 @@ class ChatController(args: Bundle) :
     }
 
     override fun onMessageViewLongClick(view: View?, message: IMessage?) {
-        PopupMenu(
-            ContextThemeWrapper(view?.context, R.style.appActionBarPopupMenu),
-            view,
-            if (
-                message?.user?.id == currentConversation?.actorType + "/" + currentConversation?.actorId
-            ) Gravity.END else Gravity.START
-        ).apply {
-            setOnMenuItemClickListener { item ->
-                when (item?.itemId) {
-
-                    R.id.action_copy_message -> {
-                        val clipboardManager =
-                            activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
-                        val clipData = ClipData.newPlainText(
-                            resources?.getString(R.string.nc_app_product_name),
-                            message?.text
-                        )
-                        clipboardManager.setPrimaryClip(clipData)
-                        true
-                    }
-                    R.id.action_mark_as_unread -> {
-                        val chatMessage = message as ChatMessage?
-                        if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
-                            ncApi!!.setChatReadMarker(
-                                credentials,
-                                ApiUtils.getUrlForSetChatReadMarker(
-                                    ApiUtils.getChatApiVersion(conversationUser, intArrayOf(ApiUtils.APIv1)),
-                                    conversationUser?.baseUrl,
-                                    roomToken
-                                ),
-                                chatMessage.previousMessageId
-                            )
-                                .subscribeOn(Schedulers.io())
-                                .observeOn(AndroidSchedulers.mainThread())
-                                .subscribe(object : Observer<GenericOverall> {
-                                    override fun onSubscribe(d: Disposable) {
-                                        // unused atm
-                                    }
+        if (hasVisibleItems(message as ChatMessage)) {
+            activity?.let {
+                MessageActionsDialog(
+                    activity!!,
+                    this,
+                    message,
+                    conversationUser?.userId,
+                    currentConversation,
+                    isShowMessageDeletionButton(message)
+                ).show()
+            }
+        }
+    }
 
-                                    override fun onNext(t: GenericOverall) {
-                                        // unused atm
-                                    }
+    fun deleteMessage(message: IMessage?) {
+        var apiVersion = 1
+        // FIXME Fix API checking with guests?
+        if (conversationUser != null) {
+            apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
+        }
 
-                                    override fun onError(e: Throwable) {
-                                        Log.e(TAG, e.message, e)
-                                    }
+        ncApi?.deleteChatMessage(
+            credentials,
+            ApiUtils.getUrlForChatMessage(
+                apiVersion,
+                conversationUser?.baseUrl,
+                roomToken,
+                message?.id
+            )
+        )?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<ChatOverallSingleMessage> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
 
-                                    override fun onComplete() {
-                                        // unused atm
-                                    }
-                                })
-                        }
-                        true
+                override fun onNext(t: ChatOverallSingleMessage) {
+                    if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
+                        Toast.makeText(
+                            context, R.string.nc_delete_message_leaked_to_matterbridge,
+                            Toast.LENGTH_LONG
+                        ).show()
                     }
-                    R.id.action_forward_message -> {
-                        val bundle = Bundle()
-                        bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true)
-                        bundle.putString(BundleKeys.KEY_FORWARD_MSG_TEXT, message?.text)
-                        bundle.putString(BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM, roomId)
-                        router.pushController(
-                            RouterTransaction.with(ConversationsListController(bundle))
-                                .pushChangeHandler(HorizontalChangeHandler())
-                                .popChangeHandler(HorizontalChangeHandler())
-                        )
-                        true
-                    }
-                    R.id.action_reply_to_message -> {
-                        val chatMessage = message as ChatMessage?
-                        replyToMessage(chatMessage, message?.jsonMessageId)
-                        true
-                    }
-                    R.id.action_reply_privately -> {
-                        val apiVersion =
-                            ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
-                        val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
-                            apiVersion,
-                            conversationUser?.baseUrl,
-                            "1",
-                            null,
-                            message?.user?.id?.substring(INVITE_LENGTH),
-                            null
-                        )
-                        ncApi!!.createRoom(
-                            credentials,
-                            retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
-                        )
-                            .subscribeOn(Schedulers.io())
-                            .observeOn(AndroidSchedulers.mainThread())
-                            .subscribe(object : Observer<RoomOverall> {
-                                override fun onSubscribe(d: Disposable) {
-                                    // unused atm
-                                }
+                }
 
-                                override fun onNext(roomOverall: RoomOverall) {
-                                    val bundle = Bundle()
-                                    bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
-                                    bundle.putString(KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
-                                    bundle.putString(KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
-
-                                    // FIXME once APIv2+ is used only, the createRoom already returns all the data
-                                    ncApi!!.getRoom(
-                                        credentials,
-                                        ApiUtils.getUrlForRoom(
-                                            apiVersion, conversationUser?.baseUrl,
-                                            roomOverall.getOcs().getData().getToken()
-                                        )
-                                    )
-                                        .subscribeOn(Schedulers.io())
-                                        .observeOn(AndroidSchedulers.mainThread())
-                                        .subscribe(object : Observer<RoomOverall> {
-                                            override fun onSubscribe(d: Disposable) {
-                                                // unused atm
-                                            }
-
-                                            override fun onNext(roomOverall: RoomOverall) {
-                                                bundle.putParcelable(
-                                                    KEY_ACTIVE_CONVERSATION,
-                                                    Parcels.wrap(roomOverall.getOcs().getData())
-                                                )
-                                                remapChatController(
-                                                    router, conversationUser!!.id,
-                                                    roomOverall.getOcs().getData().getToken(), bundle, true
-                                                )
-                                            }
-
-                                            override fun onError(e: Throwable) {
-                                                Log.e(TAG, e.message, e)
-                                            }
-
-                                            override fun onComplete() {
-                                                // unused atm
-                                            }
-                                        })
-                                }
+                override fun onError(e: Throwable) {
+                    Log.e(
+                        TAG,
+                        "Something went wrong when trying to delete message with id " +
+                            message?.id,
+                        e
+                    )
+                    Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
+                }
 
-                                override fun onError(e: Throwable) {
-                                    Log.e(TAG, e.message, e)
-                                }
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
 
-                                override fun onComplete() {
-                                    // unused atm
-                                }
-                            })
-                        true
-                    }
-                    R.id.action_delete_message -> {
-                        var apiVersion = 1
-                        // FIXME Fix API checking with guests?
-                        if (conversationUser != null) {
-                            apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
-                        }
+    fun replyPrivately(message: IMessage?) {
+        val apiVersion =
+            ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
+            apiVersion,
+            conversationUser?.baseUrl,
+            "1",
+            null,
+            message?.user?.id?.substring(INVITE_LENGTH),
+            null
+        )
+        ncApi!!.createRoom(
+            credentials,
+            retrofitBucket.getUrl(), retrofitBucket.getQueryMap()
+        )
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<RoomOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
 
-                        ncApi?.deleteChatMessage(
-                            credentials,
-                            ApiUtils.getUrlForChatMessage(
-                                apiVersion,
-                                conversationUser?.baseUrl,
-                                roomToken,
-                                message?.id
-                            )
-                        )?.subscribeOn(Schedulers.io())
-                            ?.observeOn(AndroidSchedulers.mainThread())
-                            ?.subscribe(object : Observer<ChatOverallSingleMessage> {
-                                override fun onSubscribe(d: Disposable) {
-                                    // unused atm
-                                }
+                override fun onNext(roomOverall: RoomOverall) {
+                    val bundle = Bundle()
+                    bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
+                    bundle.putString(KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken())
+                    bundle.putString(KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId())
+
+                    // FIXME once APIv2+ is used only, the createRoom already returns all the data
+                    ncApi!!.getRoom(
+                        credentials,
+                        ApiUtils.getUrlForRoom(
+                            apiVersion, conversationUser?.baseUrl,
+                            roomOverall.getOcs().getData().getToken()
+                        )
+                    )
+                        .subscribeOn(Schedulers.io())
+                        .observeOn(AndroidSchedulers.mainThread())
+                        .subscribe(object : Observer<RoomOverall> {
+                            override fun onSubscribe(d: Disposable) {
+                                // unused atm
+                            }
 
-                                override fun onNext(t: ChatOverallSingleMessage) {
-                                    if (t.ocs.meta.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
-                                        Toast.makeText(
-                                            context, R.string.nc_delete_message_leaked_to_matterbridge,
-                                            Toast.LENGTH_LONG
-                                        ).show()
-                                    }
-                                }
+                            override fun onNext(roomOverall: RoomOverall) {
+                                bundle.putParcelable(
+                                    KEY_ACTIVE_CONVERSATION,
+                                    Parcels.wrap(roomOverall.getOcs().getData())
+                                )
+                                remapChatController(
+                                    router, conversationUser!!.id,
+                                    roomOverall.getOcs().getData().getToken(), bundle, true
+                                )
+                            }
 
-                                override fun onError(e: Throwable) {
-                                    Log.e(
-                                        TAG,
-                                        "Something went wrong when trying to delete message with id " +
-                                            message?.id,
-                                        e
-                                    )
-                                    Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
-                                }
+                            override fun onError(e: Throwable) {
+                                Log.e(TAG, e.message, e)
+                            }
 
-                                override fun onComplete() {
-                                    // unused atm
-                                }
-                            })
-                        true
-                    }
-                    else -> false
+                            override fun onComplete() {
+                                // unused atm
+                            }
+                        })
                 }
-            }
-            inflate(R.menu.chat_message_menu)
-            menu.findItem(R.id.action_copy_message).isVisible = !(message as ChatMessage).isDeleted
-            menu.findItem(R.id.action_reply_to_message).isVisible = message.replyable
-            menu.findItem(R.id.action_reply_privately).isVisible = message.replyable &&
-                conversationUser?.userId?.isNotEmpty() == true && conversationUser.userId != "?" &&
-                message.user.id.startsWith("users/") &&
-                message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
-                currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
-            menu.findItem(R.id.action_delete_message).isVisible = isShowMessageDeletionButton(message)
-            menu.findItem(R.id.action_forward_message).isVisible =
-                ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getMessageType()
-            menu.findItem(R.id.action_mark_as_unread).isVisible = message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
-                ChatMessage.MessageType.SYSTEM_MESSAGE != message.getMessageType() && BuildConfig.DEBUG
-            if (menu.hasVisibleItems()) {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-                    setForceShowIcon(true)
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, e.message, e)
                 }
-                show()
-            }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    fun forwardMessage(message: IMessage?) {
+        val bundle = Bundle()
+        bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true)
+        bundle.putString(BundleKeys.KEY_FORWARD_MSG_TEXT, message?.text)
+        bundle.putString(BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM, roomId)
+        router.pushController(
+            RouterTransaction.with(ConversationsListController(bundle))
+                .pushChangeHandler(HorizontalChangeHandler())
+                .popChangeHandler(HorizontalChangeHandler())
+        )
+    }
+
+    fun markAsUnread(message: IMessage?) {
+        val chatMessage = message as ChatMessage?
+        if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
+            ncApi!!.setChatReadMarker(
+                credentials,
+                ApiUtils.getUrlForSetChatReadMarker(
+                    ApiUtils.getChatApiVersion(conversationUser, intArrayOf(ApiUtils.APIv1)),
+                    conversationUser?.baseUrl,
+                    roomToken
+                ),
+                chatMessage.previousMessageId
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe(object : Observer<GenericOverall> {
+                    override fun onSubscribe(d: Disposable) {
+                        // unused atm
+                    }
+
+                    override fun onNext(t: GenericOverall) {
+                        // unused atm
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, e.message, e)
+                    }
+
+                    override fun onComplete() {
+                        // unused atm
+                    }
+                })
         }
     }
 
-    private fun replyToMessage(chatMessage: ChatMessage?, jsonMessageId: Int?) {
+    fun copyMessage(message: IMessage?) {
+        val clipboardManager =
+            activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+        val clipData = ClipData.newPlainText(
+            resources?.getString(R.string.nc_app_product_name),
+            message?.text
+        )
+        clipboardManager.setPrimaryClip(clipData)
+    }
+
+    private fun hasVisibleItems(message: ChatMessage): Boolean {
+        return !message.isDeleted || // copy message
+            message.replyable || // reply to
+            message.replyable && // reply privately
+            conversationUser?.userId?.isNotEmpty() == true && conversationUser?.userId != "?" &&
+            message.user.id.startsWith("users/") &&
+            message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
+            currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
+            isShowMessageDeletionButton(message) || // delete
+            ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getMessageType() || // forward
+            message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread
+            ChatMessage.MessageType.SYSTEM_MESSAGE != message.getMessageType() &&
+            BuildConfig.DEBUG
+    }
+
+    fun replyToMessage(message: IMessage?) {
+        val chatMessage = message as ChatMessage?
         chatMessage?.let {
             binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
                 View.GONE
@@ -2665,7 +2654,7 @@ class ChatController(args: Bundle) :
             val quotedChatMessageView = binding
                 .messageInputView
                 .findViewById<RelativeLayout>(R.id.quotedChatMessageView)
-            quotedChatMessageView?.tag = jsonMessageId
+            quotedChatMessageView?.tag = message?.jsonMessageId
             quotedChatMessageView?.visibility = View.VISIBLE
         }
     }

+ 157 - 0
app/src/main/java/com/nextcloud/talk/ui/dialog/MessageActionsDialog.kt

@@ -0,0 +1,157 @@
+/*
+ * 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.ui.dialog
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.nextcloud.talk.BuildConfig
+import com.nextcloud.talk.R
+import com.nextcloud.talk.controllers.ChatController
+import com.nextcloud.talk.databinding.DialogMessageActionsBinding
+import com.nextcloud.talk.models.json.chat.ChatMessage
+import com.nextcloud.talk.models.json.conversations.Conversation
+
+class MessageActionsDialog(
+    val activity: Activity,
+    private val chatController: ChatController,
+    private val message: ChatMessage,
+    private val userId: String?,
+    private val currentConversation: Conversation?,
+    private val showMessageDeletionButton: Boolean
+) : BottomSheetDialog(activity) {
+
+    private lateinit var dialogMessageActionsBinding: DialogMessageActionsBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        dialogMessageActionsBinding = DialogMessageActionsBinding.inflate(layoutInflater)
+        setContentView(dialogMessageActionsBinding.root)
+        window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+
+        initMenuItemCopy(!message.isDeleted)
+        initMenuReplyToMessage(message.replyable)
+        initMenuReplyPrivately(
+            message.replyable &&
+                userId?.isNotEmpty() == true &&
+                userId != "?" &&
+                message.user.id.startsWith("users/") &&
+                message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
+                currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
+        )
+        initMenuDeleteMessage(showMessageDeletionButton)
+        initMenuForwardMessage(ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getMessageType())
+        initMenuMarkAsUnread(
+            message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
+                ChatMessage.MessageType.SYSTEM_MESSAGE != message.getMessageType() &&
+                BuildConfig.DEBUG
+        )
+    }
+
+    private fun initMenuMarkAsUnread(visible: Boolean) {
+        if (visible) {
+            dialogMessageActionsBinding.menuMarkAsUnread.setOnClickListener {
+                chatController.markAsUnread(message)
+                dismiss()
+            }
+        }
+
+        dialogMessageActionsBinding.menuMarkAsUnread.visibility = getVisibility(visible)
+    }
+
+    private fun initMenuForwardMessage(visible: Boolean) {
+        if (visible) {
+            dialogMessageActionsBinding.menuForwardMessage.setOnClickListener {
+                chatController.forwardMessage(message)
+                dismiss()
+            }
+        }
+
+        dialogMessageActionsBinding.menuForwardMessage.visibility = getVisibility(visible)
+    }
+
+    private fun initMenuDeleteMessage(visible: Boolean) {
+        if (visible) {
+            dialogMessageActionsBinding.menuDeleteMessage.setOnClickListener {
+                chatController.deleteMessage(message)
+                dismiss()
+            }
+        }
+
+        dialogMessageActionsBinding.menuDeleteMessage.visibility = getVisibility(visible)
+    }
+
+    private fun initMenuReplyPrivately(visible: Boolean) {
+        if (visible) {
+            dialogMessageActionsBinding.menuReplyPrivately.setOnClickListener {
+                chatController.replyPrivately(message)
+                dismiss()
+            }
+        }
+
+        dialogMessageActionsBinding.menuReplyPrivately.visibility = getVisibility(visible)
+    }
+
+    private fun initMenuReplyToMessage(visible: Boolean) {
+        if (visible) {
+            dialogMessageActionsBinding.menuReplyToMessage.setOnClickListener {
+                chatController.replyToMessage(message)
+                dismiss()
+            }
+        }
+
+        dialogMessageActionsBinding.menuReplyToMessage.visibility = getVisibility(visible)
+    }
+
+    private fun initMenuItemCopy(visible: Boolean) {
+        if (visible) {
+            dialogMessageActionsBinding.menuCopyMessage.setOnClickListener {
+                chatController.copyMessage(message)
+                dismiss()
+            }
+        }
+
+        dialogMessageActionsBinding.menuCopyMessage.visibility = getVisibility(visible)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        val bottomSheet = findViewById<View>(R.id.design_bottom_sheet)
+        val behavior = BottomSheetBehavior.from(bottomSheet as View)
+        behavior.state = BottomSheetBehavior.STATE_EXPANDED
+    }
+
+    private fun getVisibility(visible: Boolean): Int {
+        return if (visible) {
+            View.VISIBLE
+        } else {
+            View.GONE
+        }
+    }
+
+    companion object {
+        private const val ACTOR_LENGTH = 6
+        private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
+    }
+}

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

@@ -49,8 +49,8 @@
 
         <ImageView
             android:id="@+id/audio_output_bluetooth_icon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
+            android:layout_width="11dp"
+            android:layout_height="12dp"
             android:contentDescription="@null"
             android:src="@drawable/ic_baseline_bluetooth_audio_24"
             app:tint="@color/grey_600" />

+ 218 - 0
app/src/main/res/layout/dialog_message_actions.xml

@@ -0,0 +1,218 @@
+<?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: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:background="@color/bg_bottom_sheet"
+    android:orientation="vertical"
+    android:paddingStart="@dimen/standard_padding"
+    android:paddingEnd="@dimen/standard_padding"
+    android:paddingBottom="@dimen/standard_half_padding">
+
+    <LinearLayout
+        android:id="@+id/menu_copy_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_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" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/menu_mark_as_unread"
+        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_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_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"
+            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_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"
+            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_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"
+            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_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"
+            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>

+ 0 - 40
app/src/main/res/menu/chat_message_menu.xml

@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item
-        android:id="@+id/action_copy_message"
-        android:icon="@drawable/ic_content_copy"
-        android:title="@string/nc_copy_message"
-        app:showAsAction="always" />
-
-    <item
-        android:id="@+id/action_mark_as_unread"
-        android:icon="@drawable/ic_eye_off"
-        android:title="@string/nc_mark_as_unread"
-        app:showAsAction="always" />
-
-    <item
-        android:id="@+id/action_forward_message"
-        android:icon="@drawable/ic_share_action"
-        android:title="@string/nc_forward_message"
-        app:showAsAction="always" />
-
-    <item
-        android:id="@+id/action_reply_to_message"
-        android:icon="@drawable/ic_reply"
-        android:title="@string/nc_reply"
-        app:showAsAction="always" />
-
-    <item
-        android:id="@+id/action_reply_privately"
-        android:icon="@drawable/ic_reply"
-        android:title="@string/nc_reply_privately"
-        app:showAsAction="always" />
-
-    <item
-        android:id="@+id/action_delete_message"
-        android:icon="@drawable/ic_delete"
-        android:title="@string/nc_delete_message"
-        app:showAsAction="always" />
-</menu>