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 {
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"
}
}

View File

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

View File

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

View File

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

View File

@ -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()
}
}

View File

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

View File

@ -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()));
}

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

View File

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

View File

@ -19,15 +19,22 @@
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<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:orientation="vertical">
android:layout_marginBottom="4dp"
android:visibility="gone"/>
<androidx.emoji.widget.EmojiEditText
android:id="@id/messageInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_below="@+id/quotedChatMessageView"
android:layout_centerHorizontal="true"
android:layout_toStartOf="@id/sendButtonSpace"
android:layout_toEndOf="@id/attachmentButtonSpace"
android:imeOptions="actionDone"
@ -38,14 +45,14 @@
android:id="@id/attachmentButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_centerVertical="true"
android:layout_below="@id/quotedChatMessageView"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/smileyButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_centerVertical="true"
android:layout_below="@id/quotedChatMessageView"
android:layout_toStartOf="@id/messageSendButton"
android:background="@color/transparent"
android:src="@drawable/ic_insert_emoticon_black_24dp"
@ -56,21 +63,25 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_below="@id/quotedChatMessageView"
android:adjustViewBounds="true"
android:padding="4dp"
android:scaleType="centerInside" />
<androidx.legacy.widget.Space
<Space
android:id="@id/attachmentButtonSpace"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_below="@id/quotedChatMessageView"
android:layout_toEndOf="@id/attachmentButton" />
<androidx.legacy.widget.Space
<Space
android:id="@id/sendButtonSpace"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_below="@id/quotedChatMessageView"
android:layout_toStartOf="@id/smileyButton" />
</RelativeLayout>
</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_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>