فهرست منبع

Implemented most of message replies

# Conflicts:
#	app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
#	app/src/main/res/layout/view_message_input.xml
#	app/src/main/res/values/strings.xml
Mario Danic 5 سال پیش
والد
کامیت
d21d5f51b4

+ 12 - 4
app/build.gradle

@@ -30,13 +30,13 @@ if (taskRequest.contains("Gplay") || taskRequest.contains("findbugs") || taskReq
 }
 
 android {
-    compileSdkVersion 28
+    compileSdkVersion 29
     buildToolsVersion '28.0.3'
     defaultConfig {
         applicationId "com.nextcloud.talk2"
         versionName version
         minSdkVersion 21
-        targetSdkVersion 28
+        targetSdkVersion 29
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 
         versionCode 115
@@ -213,7 +213,7 @@ dependencies {
     implementation 'me.zhanghai.android.effortlesspermissions:library:1.1.0'
     implementation 'org.apache.commons:commons-lang3:3.9'
     implementation 'com.github.wooplr:Spotlight:1.3'
-    implementation('com.github.mario:chatkit:a183142049', {
+    implementation('com.github.mario:chatkit:c6a61767291ddb212a2f4f792a2b6aaf295e69a5', {
         exclude group: 'com.facebook.fresco'
     })
 
@@ -223,7 +223,9 @@ dependencies {
     implementation 'com.github.mario.fresco:animated-gif:111'
     implementation 'com.github.mario.fresco:imagepipeline-okhttp3:111'
     implementation  group: 'joda-time', name: 'joda-time', version: '2.10.3'
-
+    implementation 'io.coil-kt:coil:0.9.1'
+    implementation("io.coil-kt:coil-gif:0.9.1")
+    implementation("io.coil-kt:coil-svg:0.9.1")
     implementation 'com.github.natario1:Autocomplete:v1.1.0'
 
     implementation 'com.github.cotechde.hwsecurity:hwsecurity-fido:2.4.5'
@@ -252,3 +254,9 @@ dependencies {
     findbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.9.0'
     findbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.6'
 }
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+}

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java

@@ -217,7 +217,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
                         DraweeController draweeController = Fresco.newDraweeControllerBuilder()
                                 .setOldController(holder.dialogAvatar.getController())
                                 .setAutoPlayAnimations(true)
-                                .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(), conversation.getName(), R.dimen.avatar_size), null))
+                                .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userEntity.getBaseUrl(), conversation.getName(), R.dimen.avatar_size), userEntity))
                                 .build();
                         holder.dialogAvatar.setController(draweeController);
                     } else {

+ 2 - 1
app/src/main/java/com/nextcloud/talk/api/NcApi.java

@@ -283,7 +283,8 @@ public interface NcApi {
     @POST
     Observable<GenericOverall> sendChatMessage(@Header("Authorization") String authorization, @Url String url,
                                                @Field("message") CharSequence message,
-                                               @Field("actorDisplayName") String actorDisplayName);
+                                               @Field("actorDisplayName") String actorDisplayName,
+                                               @Field("replyTo") Integer replyTo);
 
     @GET
     Observable<MentionOverall> getMentionAutocompleteSuggestions(@Header("Authorization") String authorization,

+ 23 - 0
app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt

@@ -22,6 +22,8 @@ package com.nextcloud.talk.application
 
 import android.content.Context
 import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.P
 import android.util.Log
 import androidx.emoji.bundled.BundledEmojiCompatConfig
 import androidx.emoji.text.EmojiCompat
@@ -36,6 +38,11 @@ import androidx.work.PeriodicWorkRequest
 import androidx.work.WorkManager
 import autodagger.AutoComponent
 import autodagger.AutoInjector
+import coil.Coil
+import coil.ImageLoader
+import coil.decode.GifDecoder
+import coil.decode.ImageDecoderDecoder
+import coil.decode.SvgDecoder
 import com.facebook.cache.disk.DiskCacheConfig
 import com.facebook.drawee.backends.pipeline.Fresco
 import com.facebook.imagepipeline.core.ImagePipelineConfig
@@ -130,6 +137,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
 
         componentApplication.inject(this)
 
+        Coil.setDefaultImageLoader(::buildDefaultImageLoader)
         setAppTheme(appPreferences.theme)
         super.onCreate()
 
@@ -192,6 +200,21 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
         MultiDex.install(this)
     }
 
+    private fun buildDefaultImageLoader(): ImageLoader {
+        return ImageLoader(applicationContext) {
+            availableMemoryPercentage(0.5) // Use 50% of the application's available memory.
+            crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView.
+            componentRegistry {
+                if (SDK_INT >= P) {
+                    add(ImageDecoderDecoder())
+                } else {
+                    add(GifDecoder())
+                }
+                add(SvgDecoder(applicationContext))
+            }
+            okHttpClient(okHttpClient)
+        }
+    }
     companion object {
         private val TAG = NextcloudTalkApplication::class.java.simpleName
         //region Singleton

+ 91 - 17
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -20,13 +20,15 @@
 
 package com.nextcloud.talk.controllers
 
-
+import android.content.ClipData
 import android.content.Context
 import android.content.Intent
 import android.content.res.Resources
 import android.graphics.Bitmap
 import android.graphics.PorterDuff
 import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.os.Build
 import android.os.Bundle
 import android.os.Handler
 import android.os.Parcelable
@@ -35,16 +37,20 @@ import android.text.InputFilter
 import android.text.TextUtils
 import android.text.TextWatcher
 import android.util.Log
+import android.util.TypedValue
 import android.view.*
 import android.widget.*
 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
 import androidx.emoji.text.EmojiCompat
 import androidx.emoji.widget.EmojiEditText
+import androidx.emoji.widget.EmojiTextView
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import autodagger.AutoInjector
 import butterknife.BindView
 import butterknife.OnClick
+import coil.api.load
+import coil.transform.CircleCropTransformation
 import com.bluelinelabs.conductor.RouterTransaction
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
 import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
@@ -54,6 +60,7 @@ import com.facebook.datasource.DataSource
 import com.facebook.drawee.backends.pipeline.Fresco
 import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
 import com.facebook.imagepipeline.image.CloseableImage
+import com.google.android.flexbox.FlexboxLayout
 import com.nextcloud.talk.R
 import com.nextcloud.talk.activities.MagicCallActivity
 import com.nextcloud.talk.adapters.messages.*
@@ -108,7 +115,7 @@ import javax.inject.Inject
 @AutoInjector(NextcloudTalkApplication::class)
 class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
 .OnLoadMoreListener, MessagesListAdapter.Formatter<Date>, MessagesListAdapter
-.OnMessageLongClickListener<IMessage>, MessageHolders.ContentChecker<IMessage> {
+.OnMessageViewLongClickListener<IMessage>, MessageHolders.ContentChecker<IMessage> {
 
     @Inject
     @JvmField
@@ -150,6 +157,9 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
     @JvmField
     var conversationLobbyText: TextView? = null
     val disposableList = ArrayList<Disposable>()
+    @JvmField
+    @BindView(R.id.quotedChatMessageView)
+    var quotedChatMessageView: RelativeLayout? = null
     var roomToken: String? = null
     val conversationUser: UserEntity?
     val roomPassword: String
@@ -202,7 +212,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         if (conversationUser?.userId == "?") {
             credentials = null
         } else {
-            credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token)
+            credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
         }
 
         if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
@@ -300,7 +310,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
                     .intrinsicWidth.toFloat(), activity).toInt()
 
             val imageRequest = DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameAndPixels(conversationUser?.baseUrl,
-                    currentConversation?.name, avatarSize / 2), null)
+                    currentConversation?.name, avatarSize / 2), conversationUser!!)
 
             val imagePipeline = Fresco.getImagePipeline()
             val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
@@ -362,7 +372,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         messagesListView?.setAdapter(adapter)
         adapter?.setLoadMoreListener(this)
         adapter?.setDateHeadersFormatter { format(it) }
-        adapter?.setOnMessageLongClickListener { onMessageLongClick(it) }
+        adapter?.setOnMessageViewLongClickListener { view, message ->  onMessageViewLongClick(view, message)}
 
         layoutManager = messagesListView?.layoutManager as LinearLayoutManager?
 
@@ -403,7 +413,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         val filters = arrayOfNulls<InputFilter>(1)
         val lengthFilter = conversationUser?.messageMaxLength ?: 1000
 
-
         filters[0] = InputFilter.LengthFilter(lengthFilter)
         messageInput?.filters = filters
 
@@ -831,16 +840,22 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
             }
 
             messageInput?.setText("")
-            sendMessage(editable)
+            val replyMessageId: Long? = view?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Long?
+            sendMessage(editable, if (view?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.visibility == View.VISIBLE) replyMessageId?.toInt() else null )
+            cancelReply()
         }
     }
 
-    private fun sendMessage(message: CharSequence) {
+    private fun sendMessage(message: CharSequence, replyTo: Int?) {
 
         if (conversationUser != null) {
-            ncApi?.sendChatMessage(credentials, ApiUtils.getUrlForChat(conversationUser?.baseUrl,
-                    roomToken),
-                    message, conversationUser.displayName)
+            ncApi!!.sendChatMessage(
+                    credentials, ApiUtils.getUrlForChat(
+                    conversationUser.baseUrl,
+                    roomToken
+            ),
+                    message, conversationUser.displayName, replyTo
+            )
                     ?.subscribeOn(Schedulers.io())
                     ?.observeOn(AndroidSchedulers.mainThread())
                     ?.subscribe(object : Observer<GenericOverall> {
@@ -1240,12 +1255,71 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
         }
     }
 
-    override fun onMessageLongClick(message: IMessage) {
-        if (activity != null) {
-            val clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
-            val clipData = android.content.ClipData.newPlainText(
-                    resources?.getString(R.string.nc_app_name), message.text)
-            clipboardManager.primaryClip = clipData
+    @OnClick(R.id.cancelReplyButton)
+    fun cancelReply() {
+        quotedChatMessageView?.visibility = View.GONE
+        messageInputView?.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
+        messageInputView?.findViewById<Space>(R.id.attachmentButtonSpace)?.visibility = View.VISIBLE
+    }
+
+    override fun onMessageViewLongClick(view: View?, message: IMessage?) {
+        PopupMenu(this.context, view, if (message?.user?.id == conversationUser?.userId) 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_name), message?.text)
+                        clipboardManager.setPrimaryClip(clipData)
+                        true
+                    }
+                    R.id.action_reply_to_message -> {
+                        val chatMessage = message as ChatMessage?
+                        chatMessage?.let {
+                            messageInputView?.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.GONE
+                            messageInputView?.findViewById<Space>(R.id.attachmentButtonSpace)?.visibility = View.GONE
+                            messageInputView?.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility = View.VISIBLE
+                            messageInputView?.findViewById<EmojiTextView>(R.id.quotedMessage)?.maxLines = 2
+                            messageInputView?.findViewById<EmojiTextView>(R.id.quotedMessage)?.ellipsize = TextUtils.TruncateAt.END
+                            messageInputView?.findViewById<EmojiTextView>(R.id.quotedMessage)?.text = it.text
+                            messageInputView?.findViewById<TextView>(R.id.quotedMessageTime)?.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME)
+                            messageInputView?.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text = it.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest)
+
+                            conversationUser?.let { currentUser ->
+
+                                messageInputView?.findViewById<ImageView>(R.id.quotedUserAvatar)?.load(it.user.avatar) {
+                                    addHeader("Authorization", credentials!!)
+                                    transformations(CircleCropTransformation())
+                                }
+
+                                chatMessage.imageUrl?.let{ previewImageUrl ->
+                                    messageInputView?.findViewById<ImageView>(R.id.quotedMessageImage)?.visibility = View.VISIBLE
+
+                                    val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96f, resources?.displayMetrics)
+                                    messageInputView?.findViewById<ImageView>(R.id.quotedMessageImage)?.maxHeight = px.toInt()
+                                    val layoutParams = messageInputView?.findViewById<ImageView>(R.id.quotedMessageImage)?.layoutParams as FlexboxLayout.LayoutParams
+                                    layoutParams.flexGrow = 0f
+                                    messageInputView?.findViewById<ImageView>(R.id.quotedMessageImage)?.layoutParams = layoutParams
+                                    messageInputView?.findViewById<ImageView>(R.id.quotedMessageImage)?.load(previewImageUrl) {
+                                        addHeader("Authorization", credentials!!)
+                                    }
+                                } ?: run {
+                                    messageInputView?.findViewById<ImageView>(R.id.quotedMessageImage)?.visibility = View.GONE
+                                }
+                            }
+
+                            quotedChatMessageView?.tag = message?.jsonMessageId
+                            quotedChatMessageView?.visibility = View.VISIBLE
+                        }
+                        true
+                    }
+                    else -> false
+                }
+            }
+            inflate(R.menu.chat_message_menu)
+            menu.findItem(R.id.action_reply_to_message).isVisible = (message as ChatMessage).replyable
+            show()
         }
     }
 

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

@@ -574,7 +574,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                         .setOldController(conversationAvatarImageView.controller)
                         .setAutoPlayAnimations(true)
                         .setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(conversationUser!!.baseUrl,
-                                conversation!!.name, R.dimen.avatar_size_big), null))
+                                conversation!!.name, R.dimen.avatar_size_big), conversationUser))
                         .build()
                 conversationAvatarImageView.controller = draweeController
             }

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

@@ -161,7 +161,7 @@ public class DisplayUtils {
 
     public static ImageRequest getImageRequestForUrl(String url, @Nullable UserEntity userEntity) {
         Map<String, String> headers = new HashMap<>();
-        if (userEntity != null && url.startsWith(userEntity.getBaseUrl()) && url.contains("index.php/core/preview?fileId=")) {
+        if (userEntity != null && url.startsWith(userEntity.getBaseUrl()) && (url.contains("index.php/core/preview?fileId=") || url.contains("/avatar/"))) {
             headers.put("Authorization", ApiUtils.getCredentials(userEntity.getUsername(), userEntity.getToken()));
         }
 

+ 5 - 0
app/src/main/res/drawable/ic_content_copy_white_24dp.xml

@@ -0,0 +1,5 @@
+<vector android:autoMirrored="true" android:height="24dp"
+    android:tint="#FFFFFF" android:viewportHeight="24.0"
+    android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
+</vector>

+ 5 - 0
app/src/main/res/drawable/ic_reply_white_24dp.xml

@@ -0,0 +1,5 @@
+<vector android:autoMirrored="true" android:height="24dp"
+    android:tint="#FFFFFF" android:viewportHeight="24.0"
+    android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"/>
+</vector>

+ 3 - 1
app/src/main/res/layout/controller_chat.xml

@@ -22,7 +22,8 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:keepScreenOn="true">
+    android:keepScreenOn="true"
+    android:animateLayoutChanges="true">
 
     <include layout="@layout/lobby_view"
         android:visibility="gone"/>
@@ -52,6 +53,7 @@
 
     <com.stfalcon.chatkit.messages.MessageInput
         android:id="@+id/messageInputView"
+        android:animateLayoutChanges="true"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"

+ 3 - 1
app/src/main/res/layout/controller_conversations_rv.xml

@@ -89,8 +89,10 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="bottom|end"
+        android:backgroundTint="@color/colorPrimary"
         android:layout_margin="16dp"
-        app:srcCompat="@drawable/ic_add_white_24px" />
+        app:tint="@color/white"
+        app:srcCompat="@drawable/ic_add_white_24px"/>
 
     <include layout="@layout/fast_scroller" />
 

+ 57 - 46
app/src/main/res/layout/view_message_input.xml

@@ -20,57 +20,68 @@
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical">
+    android:layout_height="wrap_content">
 
-    <androidx.emoji.widget.EmojiEditText
-        android:id="@id/messageInput"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_centerVertical="true"
-        android:layout_toStartOf="@id/sendButtonSpace"
-        android:layout_toEndOf="@id/attachmentButtonSpace"
-        android:imeOptions="actionDone"
-        android:inputType="textAutoCorrect|textMultiLine|textCapSentences"
-        android:lineSpacingMultiplier="1.2" />
+    <RelativeLayout android:layout_height="wrap_content" android:layout_width="match_parent">
+        <include layout="@layout/item_message_quote"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
+            android:visibility="gone"/>
 
-    <ImageButton
-        android:id="@id/attachmentButton"
-        android:layout_width="36dp"
-        android:layout_height="36dp"
-        android:layout_centerVertical="true"
-        android:scaleType="centerInside" />
+        <androidx.emoji.widget.EmojiEditText
+            android:id="@id/messageInput"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@+id/quotedChatMessageView"
+            android:layout_centerHorizontal="true"
+            android:layout_toStartOf="@id/sendButtonSpace"
+            android:layout_toEndOf="@id/attachmentButtonSpace"
+            android:imeOptions="actionDone"
+            android:inputType="textAutoCorrect|textMultiLine|textCapSentences"
+            android:lineSpacingMultiplier="1.2" />
 
-    <ImageButton
-        android:id="@+id/smileyButton"
-        android:layout_width="36dp"
-        android:layout_height="36dp"
-        android:layout_centerVertical="true"
-        android:layout_toStartOf="@id/messageSendButton"
-        android:background="@color/transparent"
-        android:src="@drawable/ic_insert_emoticon_black_24dp"
-        android:tint="@color/emoji_icons" />
+        <ImageButton
+            android:id="@id/attachmentButton"
+            android:layout_width="36dp"
+            android:layout_height="36dp"
+            android:layout_below="@id/quotedChatMessageView"
+            android:scaleType="centerInside" />
 
-    <ImageButton
-        android:id="@id/messageSendButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentEnd="true"
-        android:layout_centerVertical="true"
-        android:adjustViewBounds="true"
-        android:padding="4dp"
-        android:scaleType="centerInside" />
+        <ImageButton
+            android:id="@+id/smileyButton"
+            android:layout_width="36dp"
+            android:layout_height="36dp"
+            android:layout_below="@id/quotedChatMessageView"
+            android:layout_toStartOf="@id/messageSendButton"
+            android:background="@color/transparent"
+            android:src="@drawable/ic_insert_emoticon_black_24dp"
+            android:tint="@color/emoji_icons" />
 
-    <androidx.legacy.widget.Space
-        android:id="@id/attachmentButtonSpace"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_toEndOf="@id/attachmentButton" />
+        <ImageButton
+            android:id="@id/messageSendButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentEnd="true"
+            android:layout_below="@id/quotedChatMessageView"
+            android:adjustViewBounds="true"
+            android:padding="4dp"
+            android:scaleType="centerInside" />
+
+        <Space
+            android:id="@id/attachmentButtonSpace"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_below="@id/quotedChatMessageView"
+            android:layout_toEndOf="@id/attachmentButton" />
+
+        <Space
+            android:id="@id/sendButtonSpace"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_below="@id/quotedChatMessageView"
+            android:layout_toStartOf="@id/smileyButton" />
+    </RelativeLayout>
 
-    <androidx.legacy.widget.Space
-        android:id="@id/sendButtonSpace"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:layout_toStartOf="@id/smileyButton" />
 
 </merge>

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

@@ -0,0 +1,17 @@
+<?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_white_24dp"
+        android:title="@string/nc_copy_message"
+        app:showAsAction="always"/>
+
+    <item
+        android:id="@+id/action_reply_to_message"
+        android:icon="@drawable/ic_reply_white_24dp"
+        android:title="@string/nc_reply"
+        app:showAsAction="always"
+        />
+</menu>

+ 24 - 1
app/src/main/res/values/strings.xml

@@ -298,5 +298,28 @@
     <string name="nc_lobby_waiting">You are currently waiting in the lobby.</string>
     <string name="nc_lobby_waiting_with_date">You are currently waiting in the lobby.\n This
         meeting is scheduled for %1$s.</string>
-    <string name="nc_manual">Manual</string>
+    <string name="nc_manual">Not set</string>
+
+    <!-- Errors -->
+    <string name="nc_no_connection_error">No connection</string>
+    <string name="nc_bad_response_error">Bad response</string>
+    <string name="nc_timeout_error">Timeout</string>
+    <string name="nc_empty_response_error">Empty response</string>
+    <string name="nc_not_defined_error">Unknown error</string>
+    <string name="nc_unauthorized_error">Unauthorized</string>
+
+    <string name="nc_general_settings">General</string>
+    <string name="nc_allow_guests">Allow guests</string>
+    <string name="nc_last_moderator_title">Could not leave conversation</string>
+    <string name="nc_last_moderator">You need to promote a new moderator before you can leave %1$s.</string>
+
+    <!-- Chat -->
+    <string name="nc_99_plus">99+</string>
+    <string name="nc_copy_message">Copy</string>
+    <string name="nc_reply">Reply</string>
+
+    <!-- Non-translatable strings -->
+
+    <string name="path_password_strike_through" translatable="false"
+        tools:override="true">M3.27,4.27L19.74,20.74</string>
 </resources>