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
This commit is contained in:
Mario Danic 2019-12-26 18:48:06 +01:00
parent 2613c1f074
commit d21d5f51b4
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
14 changed files with 245 additions and 74 deletions

View File

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

View File

@ -217,7 +217,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
DraweeController draweeController = Fresco.newDraweeControllerBuilder() DraweeController draweeController = Fresco.newDraweeControllerBuilder()
.setOldController(holder.dialogAvatar.getController()) .setOldController(holder.dialogAvatar.getController())
.setAutoPlayAnimations(true) .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(); .build();
holder.dialogAvatar.setController(draweeController); holder.dialogAvatar.setController(draweeController);
} else { } else {

View File

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

View File

@ -22,6 +22,8 @@ package com.nextcloud.talk.application
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.P
import android.util.Log import android.util.Log
import androidx.emoji.bundled.BundledEmojiCompatConfig import androidx.emoji.bundled.BundledEmojiCompatConfig
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
@ -36,6 +38,11 @@ import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import autodagger.AutoComponent import autodagger.AutoComponent
import autodagger.AutoInjector 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.cache.disk.DiskCacheConfig
import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.core.ImagePipelineConfig
@ -130,6 +137,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
componentApplication.inject(this) componentApplication.inject(this)
Coil.setDefaultImageLoader(::buildDefaultImageLoader)
setAppTheme(appPreferences.theme) setAppTheme(appPreferences.theme)
super.onCreate() super.onCreate()
@ -192,6 +200,21 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
MultiDex.install(this) 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 { companion object {
private val TAG = NextcloudTalkApplication::class.java.simpleName private val TAG = NextcloudTalkApplication::class.java.simpleName
//region Singleton //region Singleton

View File

@ -20,13 +20,15 @@
package com.nextcloud.talk.controllers package com.nextcloud.talk.controllers
import android.content.ClipData
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Parcelable import android.os.Parcelable
@ -35,16 +37,20 @@ import android.text.InputFilter
import android.text.TextUtils import android.text.TextUtils
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Log import android.util.Log
import android.util.TypedValue
import android.view.* import android.view.*
import android.widget.* import android.widget.*
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.emoji.widget.EmojiEditText import androidx.emoji.widget.EmojiEditText
import androidx.emoji.widget.EmojiTextView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import autodagger.AutoInjector import autodagger.AutoInjector
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick import butterknife.OnClick
import coil.api.load
import coil.transform.CircleCropTransformation
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler 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.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.image.CloseableImage
import com.google.android.flexbox.FlexboxLayout
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MagicCallActivity import com.nextcloud.talk.activities.MagicCallActivity
import com.nextcloud.talk.adapters.messages.* import com.nextcloud.talk.adapters.messages.*
@ -108,7 +115,7 @@ import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
.OnLoadMoreListener, MessagesListAdapter.Formatter<Date>, MessagesListAdapter .OnLoadMoreListener, MessagesListAdapter.Formatter<Date>, MessagesListAdapter
.OnMessageLongClickListener<IMessage>, MessageHolders.ContentChecker<IMessage> { .OnMessageViewLongClickListener<IMessage>, MessageHolders.ContentChecker<IMessage> {
@Inject @Inject
@JvmField @JvmField
@ -150,6 +157,9 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
@JvmField @JvmField
var conversationLobbyText: TextView? = null var conversationLobbyText: TextView? = null
val disposableList = ArrayList<Disposable>() val disposableList = ArrayList<Disposable>()
@JvmField
@BindView(R.id.quotedChatMessageView)
var quotedChatMessageView: RelativeLayout? = null
var roomToken: String? = null var roomToken: String? = null
val conversationUser: UserEntity? val conversationUser: UserEntity?
val roomPassword: String val roomPassword: String
@ -202,7 +212,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
if (conversationUser?.userId == "?") { if (conversationUser?.userId == "?") {
credentials = null credentials = null
} else { } else {
credentials = ApiUtils.getCredentials(conversationUser.username, conversationUser.token) credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
} }
if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
@ -300,7 +310,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
.intrinsicWidth.toFloat(), activity).toInt() .intrinsicWidth.toFloat(), activity).toInt()
val imageRequest = DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameAndPixels(conversationUser?.baseUrl, val imageRequest = DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameAndPixels(conversationUser?.baseUrl,
currentConversation?.name, avatarSize / 2), null) currentConversation?.name, avatarSize / 2), conversationUser!!)
val imagePipeline = Fresco.getImagePipeline() val imagePipeline = Fresco.getImagePipeline()
val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null) val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
@ -362,7 +372,7 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
messagesListView?.setAdapter(adapter) messagesListView?.setAdapter(adapter)
adapter?.setLoadMoreListener(this) adapter?.setLoadMoreListener(this)
adapter?.setDateHeadersFormatter { format(it) } adapter?.setDateHeadersFormatter { format(it) }
adapter?.setOnMessageLongClickListener { onMessageLongClick(it) } adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message)}
layoutManager = messagesListView?.layoutManager as LinearLayoutManager? layoutManager = messagesListView?.layoutManager as LinearLayoutManager?
@ -403,7 +413,6 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
val filters = arrayOfNulls<InputFilter>(1) val filters = arrayOfNulls<InputFilter>(1)
val lengthFilter = conversationUser?.messageMaxLength ?: 1000 val lengthFilter = conversationUser?.messageMaxLength ?: 1000
filters[0] = InputFilter.LengthFilter(lengthFilter) filters[0] = InputFilter.LengthFilter(lengthFilter)
messageInput?.filters = filters messageInput?.filters = filters
@ -831,16 +840,22 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
} }
messageInput?.setText("") 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) { if (conversationUser != null) {
ncApi?.sendChatMessage(credentials, ApiUtils.getUrlForChat(conversationUser?.baseUrl, ncApi!!.sendChatMessage(
roomToken), credentials, ApiUtils.getUrlForChat(
message, conversationUser.displayName) conversationUser.baseUrl,
roomToken
),
message, conversationUser.displayName, replyTo
)
?.subscribeOn(Schedulers.io()) ?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<GenericOverall> { ?.subscribe(object : Observer<GenericOverall> {
@ -1240,12 +1255,71 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter
} }
} }
override fun onMessageLongClick(message: IMessage) { @OnClick(R.id.cancelReplyButton)
if (activity != null) { fun cancelReply() {
val clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager quotedChatMessageView?.visibility = View.GONE
val clipData = android.content.ClipData.newPlainText( messageInputView?.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
resources?.getString(R.string.nc_app_name), message.text) messageInputView?.findViewById<Space>(R.id.attachmentButtonSpace)?.visibility = View.VISIBLE
clipboardManager.primaryClip = clipData }
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()
} }
} }

View File

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

View File

@ -161,7 +161,7 @@ public class DisplayUtils {
public static ImageRequest getImageRequestForUrl(String url, @Nullable UserEntity userEntity) { public static ImageRequest getImageRequestForUrl(String url, @Nullable UserEntity userEntity) {
Map<String, String> headers = new HashMap<>(); 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())); headers.put("Authorization", ApiUtils.getCredentials(userEntity.getUsername(), userEntity.getToken()));
} }

View File

@ -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>

View File

@ -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>

View File

@ -22,7 +22,8 @@
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="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true"> android:keepScreenOn="true"
android:animateLayoutChanges="true">
<include layout="@layout/lobby_view" <include layout="@layout/lobby_view"
android:visibility="gone"/> android:visibility="gone"/>
@ -52,6 +53,7 @@
<com.stfalcon.chatkit.messages.MessageInput <com.stfalcon.chatkit.messages.MessageInput
android:id="@+id/messageInputView" android:id="@+id/messageInputView"
android:animateLayoutChanges="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"

View File

@ -89,8 +89,10 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:backgroundTint="@color/colorPrimary"
android:layout_margin="16dp" 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" /> <include layout="@layout/fast_scroller" />

View File

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

View File

@ -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>

View File

@ -298,5 +298,28 @@
<string name="nc_lobby_waiting">You are currently waiting in the lobby.</string> <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 <string name="nc_lobby_waiting_with_date">You are currently waiting in the lobby.\n This
meeting is scheduled for %1$s.</string> 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> </resources>