From d21d5f51b4b4ed3c7627e33e7e8e37bedda1cb5a Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Thu, 26 Dec 2019 18:48:06 +0100 Subject: [PATCH] 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 --- app/build.gradle | 16 ++- .../talk/adapters/items/ConversationItem.java | 2 +- .../java/com/nextcloud/talk/api/NcApi.java | 3 +- .../application/NextcloudTalkApplication.kt | 23 ++++ .../talk/controllers/ChatController.kt | 108 +++++++++++++++--- .../controllers/ConversationInfoController.kt | 2 +- .../nextcloud/talk/utils/DisplayUtils.java | 2 +- .../drawable/ic_content_copy_white_24dp.xml | 5 + .../main/res/drawable/ic_reply_white_24dp.xml | 5 + app/src/main/res/layout/controller_chat.xml | 4 +- .../layout/controller_conversations_rv.xml | 4 +- .../main/res/layout/view_message_input.xml | 103 +++++++++-------- app/src/main/res/menu/chat_message_menu.xml | 17 +++ app/src/main/res/values/strings.xml | 25 +++- 14 files changed, 245 insertions(+), 74 deletions(-) create mode 100644 app/src/main/res/drawable/ic_content_copy_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_reply_white_24dp.xml create mode 100644 app/src/main/res/menu/chat_message_menu.xml diff --git a/app/build.gradle b/app/build.gradle index 078ede281..0d7fcbd03 100644 --- a/app/build.gradle +++ b/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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java index 07f0752a1..648e9d0a0 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java @@ -217,7 +217,7 @@ public class ConversationItem extends AbstractFlexibleItem 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 getMentionAutocompleteSuggestions(@Header("Authorization") String authorization, diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index 188520ba7..255f69f26 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index b2e61f98e..ae4970bd9 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/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, MessagesListAdapter -.OnMessageLongClickListener, MessageHolders.ContentChecker { +.OnMessageViewLongClickListener, MessageHolders.ContentChecker { @Inject @JvmField @@ -150,6 +157,9 @@ class ChatController(args: Bundle) : BaseController(args), MessagesListAdapter @JvmField var conversationLobbyText: TextView? = null val disposableList = ArrayList() + @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(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(R.id.quotedChatMessageView)?.tag as Long? + sendMessage(editable, if (view?.findViewById(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 { @@ -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(R.id.attachmentButton)?.visibility = View.VISIBLE + messageInputView?.findViewById(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(R.id.attachmentButton)?.visibility = View.GONE + messageInputView?.findViewById(R.id.attachmentButtonSpace)?.visibility = View.GONE + messageInputView?.findViewById(R.id.cancelReplyButton)?.visibility = View.VISIBLE + messageInputView?.findViewById(R.id.quotedMessage)?.maxLines = 2 + messageInputView?.findViewById(R.id.quotedMessage)?.ellipsize = TextUtils.TruncateAt.END + messageInputView?.findViewById(R.id.quotedMessage)?.text = it.text + messageInputView?.findViewById(R.id.quotedMessageTime)?.text = DateFormatter.format(it.createdAt, DateFormatter.Template.TIME) + messageInputView?.findViewById(R.id.quotedMessageAuthor)?.text = it.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest) + + conversationUser?.let { currentUser -> + + messageInputView?.findViewById(R.id.quotedUserAvatar)?.load(it.user.avatar) { + addHeader("Authorization", credentials!!) + transformations(CircleCropTransformation()) + } + + chatMessage.imageUrl?.let{ previewImageUrl -> + messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = View.VISIBLE + + val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96f, resources?.displayMetrics) + messageInputView?.findViewById(R.id.quotedMessageImage)?.maxHeight = px.toInt() + val layoutParams = messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams as FlexboxLayout.LayoutParams + layoutParams.flexGrow = 0f + messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams = layoutParams + messageInputView?.findViewById(R.id.quotedMessageImage)?.load(previewImageUrl) { + addHeader("Authorization", credentials!!) + } + } ?: run { + messageInputView?.findViewById(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() } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index 3837fa7f4..b0033aa46 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/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 } diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index 6f848c7a9..cd57ab347 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/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 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())); } diff --git a/app/src/main/res/drawable/ic_content_copy_white_24dp.xml b/app/src/main/res/drawable/ic_content_copy_white_24dp.xml new file mode 100644 index 000000000..c211821b4 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply_white_24dp.xml b/app/src/main/res/drawable/ic_reply_white_24dp.xml new file mode 100644 index 000000000..bfe58aa51 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/controller_chat.xml b/app/src/main/res/layout/controller_chat.xml index 04ad6a86d..e7c0546ca 100644 --- a/app/src/main/res/layout/controller_chat.xml +++ b/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"> @@ -52,6 +53,7 @@ + app:tint="@color/white" + app:srcCompat="@drawable/ic_add_white_24px"/> diff --git a/app/src/main/res/layout/view_message_input.xml b/app/src/main/res/layout/view_message_input.xml index 0825be19d..3cd50fe3a 100644 --- a/app/src/main/res/layout/view_message_input.xml +++ b/app/src/main/res/layout/view_message_input.xml @@ -20,57 +20,68 @@ + android:layout_height="wrap_content"> - + + - + - + - + - + + + + + + - diff --git a/app/src/main/res/menu/chat_message_menu.xml b/app/src/main/res/menu/chat_message_menu.xml new file mode 100644 index 000000000..9fb8c55fe --- /dev/null +++ b/app/src/main/res/menu/chat_message_menu.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d203d22c..a416de161 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -298,5 +298,28 @@ You are currently waiting in the lobby. You are currently waiting in the lobby.\n This meeting is scheduled for %1$s. - Manual + Not set + + + No connection + Bad response + Timeout + Empty response + Unknown error + Unauthorized + + General + Allow guests + Could not leave conversation + You need to promote a new moderator before you can leave %1$s. + + + 99+ + Copy + Reply + + + + M3.27,4.27L19.74,20.74